Go CLI: Add list configs and set log level command (#13983)

### What problem does this PR solve?

1. list configs
2. set log level debug/info/warn/error/fatal/panic

```

RAGFlow(user)> list configs;
+--------------------+-----------------------+
| key                | value                 |
+--------------------+-----------------------+
| redis_host         | localhost:6379        |
| doc_engine         | elasticsearch         |
| elasticsearch_host | http://localhost:1200 |
| log_level          | info                  |
| database           | mysql                 |
| database_host      | localhost:3306        |
| admin              | 0.0.0.0:9383          |
| storage_engine     | minio                 |
| minio_host         | localhost:9000        |
+--------------------+-----------------------+
```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added `LIST CONFIGS` command to view system configuration details
(Redis, database, log level, storage engine, and host settings).
* Added `SET LOG LEVEL` command to adjust logging verbosity at runtime.

* **Improvements**
* Enhanced log level configuration defaults and runtime state
management.
* Reorganized token management and system endpoints under `/system/`
routes for better API organization.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
This commit is contained in:
Jin Hai
2026-04-08 19:32:53 +08:00
committed by GitHub
parent 86900dca99
commit 5fe6f7c9ac
10 changed files with 328 additions and 51 deletions

View File

@ -89,6 +89,9 @@ func main() {
}
}
server.SetLogger(logger.Logger)
if config.Log.Level == "" {
config.Log.Level = logger.GetLevel()
}
logger.Info("Server mode", zap.String("mode", config.Server.Mode))

View File

@ -706,9 +706,9 @@ func (c *RAGFlowClient) ShowUser(cmd *Command) (ResponseIf, error) {
return &result, nil
}
// ListDatasets lists datasets for a specific user (admin mode)
// ListUserDatasets lists datasets for a specific user (admin mode)
// Returns (result_map, error) - result_map is non-nil for benchmark mode
func (c *RAGFlowClient) ListDatasets(cmd *Command) (ResponseIf, error) {
func (c *RAGFlowClient) ListUserDatasets(cmd *Command) (ResponseIf, error) {
if c.ServerType != "admin" {
return nil, fmt.Errorf("this command is only allowed in ADMIN mode")
}

View File

@ -126,8 +126,6 @@ func (c *RAGFlowClient) ExecuteAdminCommand(cmd *Command) (ResponseIf, error) {
return c.PingAdmin(cmd)
case "benchmark":
return c.RunBenchmark(cmd)
case "list_user_datasets":
return c.ListUserDatasets(cmd)
case "list_users":
return c.ListUsers(cmd)
case "list_services":
@ -150,8 +148,8 @@ func (c *RAGFlowClient) ExecuteAdminCommand(cmd *Command) (ResponseIf, error) {
return c.ShowAdminVersion(cmd)
case "show_user":
return c.ShowUser(cmd)
case "list_datasets":
return c.ListDatasets(cmd)
case "list_user_datasets":
return c.ListUserDatasets(cmd)
case "list_agents":
return c.ListAgents(cmd)
case "generate_token":
@ -185,10 +183,15 @@ func (c *RAGFlowClient) ExecuteUserCommand(cmd *Command) (ResponseIf, error) {
return c.Logout()
case "ping":
return c.PingServer(cmd)
// Configuration commands
case "list_configs":
return c.ListConfigs(cmd)
case "set_log_level":
return c.SetLogLevel(cmd)
case "benchmark":
return c.RunBenchmark(cmd)
case "list_user_datasets":
return c.ListUserDatasets(cmd)
case "list_datasets":
return c.ListDatasets(cmd)
case "search_on_datasets":
return c.SearchOnDatasets(cmd)
case "create_token":

View File

@ -400,3 +400,27 @@ func ReadPasswordFallback() (string, error) {
}
return strings.TrimSpace(password), nil
}
// FlattenMap recursively flattens a nested map into dot-notation keys
func FlattenMap(data map[string]interface{}, prefix string, result *[]map[string]interface{}) {
for key, value := range data {
// Build the current key path
currentKey := key
if prefix != "" {
currentKey = prefix + "." + key
}
// Check if the value is another nested map
if nestedMap, ok := value.(map[string]interface{}); ok {
// Recursively process the nested map
FlattenMap(nestedMap, currentKey, result)
} else {
// Leaf node: append to result slice
resultItem := map[string]interface{}{
"key": currentKey,
"value": value,
}
*result = append(*result, resultItem)
}
}
}

View File

@ -359,6 +359,22 @@ func (l *Lexer) lookupIdent(ident string) Token {
return Token{Type: TokenDocument, Value: ident}
case "TAGS":
return Token{Type: TokenTag, Value: ident}
case "LOG":
return Token{Type: TokenLog, Value: ident}
case "LEVEL":
return Token{Type: TokenLevel, Value: ident}
case "DEBUG":
return Token{Type: TokenDebug, Value: ident}
case "INFO":
return Token{Type: TokenInfo, Value: ident}
case "WARN":
return Token{Type: TokenWarn, Value: ident}
case "ERROR":
return Token{Type: TokenError, Value: ident}
case "FATAL":
return Token{Type: TokenFatal, Value: ident}
case "PANIC":
return Token{Type: TokenPanic, Value: ident}
default:
return Token{Type: TokenIdentifier, Value: ident}
}

View File

@ -122,7 +122,14 @@ const (
TokenChunk
TokenDocument
TokenTag
TokenLog
TokenLevel
TokenDebug
TokenInfo
TokenWarn
TokenError
TokenFatal
TokenPanic
// Literals
TokenIdentifier
TokenQuotedString

View File

@ -92,6 +92,189 @@ func (c *RAGFlowClient) ShowServerVersion(cmd *Command) (ResponseIf, error) {
return &result, nil
}
func (c *RAGFlowClient) ListConfigs(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in ADMIN mode")
}
// Get iterations from command params (for benchmark)
iterations := 1
if val, ok := cmd.Params["iterations"].(int); ok && val > 1 {
iterations = val
}
if iterations > 1 {
// Benchmark mode: multiple iterations
return c.HTTPClient.RequestWithIterations("GET", "/system/configs", true, "web", nil, nil, iterations)
}
// Single mode
resp, err := c.HTTPClient.Request("GET", "/system/configs", true, "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to list configs: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to list configs: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var response CommonDataResponse
if err = json.Unmarshal(resp.Body, &response); err != nil {
return nil, fmt.Errorf("list configs failed: invalid JSON (%w)", err)
}
var result CommonResponse
result.Code = 0
result.Data, err = GetConfigs(&response.Data)
if err != nil {
return nil, fmt.Errorf("failed to list configs: %w", err)
}
result.Duration = resp.Duration
return &result, nil
}
func GetConfigs(config *map[string]interface{}) ([]map[string]interface{}, error) {
if config == nil {
return nil, fmt.Errorf("config is nil")
}
result := []map[string]interface{}{}
{
redisHost := GetHost(config, "Redis", "Host", "Port")
result = append(result, map[string]interface{}{
"key": "redis_host",
"value": redisHost})
}
{
if docEngine, ok := (*config)["DocEngine"].(map[string]interface{}); ok {
engineType, _ := docEngine["Type"].(string)
result = append(result, map[string]interface{}{
"key": "doc_engine",
"value": engineType})
if engineType == "elasticsearch" {
esCfg, _ := docEngine["ES"].(map[string]interface{})
esHost, _ := esCfg["Hosts"].(string)
result = append(result, map[string]interface{}{
"key": "elasticsearch_host",
"value": esHost})
} else if engineType == "Infinity" {
infinityCfg, _ := docEngine["Infinity"].(map[string]interface{})
infinityHost, _ := infinityCfg["URI"]
result = append(result, map[string]interface{}{
"key": "infinity_host",
"value": infinityHost})
} else {
return nil, fmt.Errorf("unknown doc engine: %s", engineType)
}
}
}
{
if logConfig, ok := (*config)["Log"].(map[string]interface{}); ok {
level, _ := logConfig["Level"].(string)
result = append(result, map[string]interface{}{
"key": "log_level",
"value": level})
}
}
{
if databaseConfig, ok := (*config)["Database"].(map[string]interface{}); ok {
driver, _ := databaseConfig["Driver"].(string)
result = append(result, map[string]interface{}{
"key": "database",
"value": driver})
driverAddr, _ := databaseConfig["Host"].(string)
driverPort, _ := databaseConfig["Port"].(float64)
driverHost := fmt.Sprintf("%s:%0.f", driverAddr, driverPort)
result = append(result, map[string]interface{}{
"key": "database_host",
"value": driverHost})
}
}
{
if language, ok := (*config)["Language"].(map[string]interface{}); ok {
result = append(result, map[string]interface{}{
"key": "language",
"value": language})
}
}
{
if adminConfig, ok := (*config)["Admin"].(map[string]interface{}); ok {
adminAddr, _ := adminConfig["Host"].(string)
adminPort, _ := adminConfig["Port"].(float64)
adminHost := fmt.Sprintf("%s:%0.f", adminAddr, adminPort)
result = append(result, map[string]interface{}{
"key": "admin",
"value": adminHost})
}
}
{
if storageEngineConfig, ok := (*config)["StorageEngine"].(map[string]interface{}); ok {
engineType, _ := storageEngineConfig["Type"].(string)
result = append(result, map[string]interface{}{
"key": "storage_engine",
"value": engineType})
if engineType == "minio" {
minioCfg, _ := storageEngineConfig["Minio"].(map[string]interface{})
miniHost, _ := minioCfg["Host"].(string)
result = append(result, map[string]interface{}{
"key": "minio_host",
"value": miniHost})
} else {
return nil, fmt.Errorf("unknown storage engine: %s", engineType)
}
}
}
return result, nil
}
func GetHost(config *map[string]interface{}, serverType, address, port string) string {
if config == nil {
return ""
}
result := ""
if redis, ok := (*config)[serverType].(map[string]interface{}); ok {
serverAddr, hostOk := redis[address].(string)
serverPort, portOk := redis[port].(float64)
if hostOk && portOk {
result = fmt.Sprintf("%s:%.0f", serverAddr, serverPort)
}
}
return result
}
func (c *RAGFlowClient) SetLogLevel(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in ADMIN mode")
}
if logLevel, ok := cmd.Params["level"].(string); ok {
payload := map[string]interface{}{
"level": logLevel,
}
resp, err := c.HTTPClient.Request("PUT", "/system/log", true, "admin", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to change log level: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to register user: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result SimpleResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("change log level failed: invalid JSON (%w)", err)
}
result.Code = 0
result.Duration = resp.Duration
return &result, nil
}
return nil, fmt.Errorf("no log level")
}
func (c *RAGFlowClient) RegisterUser(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in ADMIN mode")
@ -150,9 +333,9 @@ func (c *RAGFlowClient) RegisterUser(cmd *Command) (ResponseIf, error) {
return &result, nil
}
// ListUserDatasets lists datasets for current user (user mode)
// ListDatasets lists datasets for current user (user mode)
// Returns (result_map, error) - result_map is non-nil for benchmark mode
func (c *RAGFlowClient) ListUserDatasets(cmd *Command) (ResponseIf, error) {
func (c *RAGFlowClient) ListDatasets(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
@ -164,11 +347,19 @@ func (c *RAGFlowClient) ListUserDatasets(cmd *Command) (ResponseIf, error) {
}
// Determine auth kind based on whether API token is being used
if c.HTTPClient.LoginToken == "" && !c.HTTPClient.useAPIToken {
return nil, fmt.Errorf("no authorization")
}
authKind := "web"
if c.HTTPClient.useAPIToken {
authKind = "api"
}
if c.HTTPClient.LoginToken != "" {
authKind = "web"
}
if iterations > 1 {
// Benchmark mode - return raw result for benchmark stats
return c.HTTPClient.RequestWithIterations("GET", "/datasets", true, authKind, nil, nil, iterations)
@ -382,7 +573,7 @@ func (c *RAGFlowClient) CreateToken(cmd *Command) (ResponseIf, error) {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
resp, err := c.HTTPClient.Request("POST", "/tokens", true, "web", nil, nil)
resp, err := c.HTTPClient.Request("POST", "/system/tokens", true, "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to create token: %w", err)
}
@ -413,7 +604,7 @@ func (c *RAGFlowClient) ListTokens(cmd *Command) (ResponseIf, error) {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
resp, err := c.HTTPClient.Request("GET", "/tokens", true, "web", nil, nil)
resp, err := c.HTTPClient.Request("GET", "/system/tokens", true, "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to list tokens: %w", err)
}
@ -445,7 +636,7 @@ func (c *RAGFlowClient) DropToken(cmd *Command) (ResponseIf, error) {
return nil, fmt.Errorf("token not provided")
}
resp, err := c.HTTPClient.Request("DELETE", fmt.Sprintf("/tokens/%s", token), true, "web", nil, nil)
resp, err := c.HTTPClient.Request("DELETE", fmt.Sprintf("/system/tokens/%s", token), true, "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to drop token: %w", err)
}

View File

@ -188,24 +188,9 @@ func (p *Parser) parseListCommand() (*Command, error) {
}
func (p *Parser) parseListDatasets() (*Command, error) {
cmd := NewCommand("list_user_datasets")
cmd := NewCommand("list_datasets")
p.nextToken() // consume DATASETS
if p.curToken.Type == TokenSemicolon {
return cmd, nil
}
if p.curToken.Type == TokenOf {
p.nextToken()
userName, err := p.parseQuotedString()
if err != nil {
return nil, err
}
cmd = NewCommand("list_datasets")
cmd.Params["user_name"] = userName
p.nextToken()
}
// Semicolon is optional for UNSET TOKEN
if p.curToken.Type == TokenSemicolon {
p.nextToken()
@ -243,20 +228,7 @@ func (p *Parser) parseListAgents() (*Command, error) {
func (p *Parser) parseListTokens() (*Command, error) {
p.nextToken() // consume TOKENS
if p.curToken.Type != TokenOf {
return nil, fmt.Errorf("expected OF")
}
p.nextToken()
userName, err := p.parseQuotedString()
if err != nil {
return nil, err
}
cmd := NewCommand("list_tokens")
cmd.Params["user_name"] = userName
p.nextToken()
// Semicolon is optional for UNSET TOKEN
if p.curToken.Type == TokenSemicolon {
p.nextToken()
@ -1584,6 +1556,9 @@ func (p *Parser) parseSetCommand() (*Command, error) {
if p.curToken.Type == TokenMetadata {
return p.parseSetMeta()
}
if p.curToken.Type == TokenLog {
return p.parseSetLog()
}
return nil, fmt.Errorf("unknown SET target: %s", p.curToken.Value)
}
@ -1673,6 +1648,45 @@ func (p *Parser) parseSetToken() (*Command, error) {
return cmd, nil
}
func (p *Parser) parseSetLog() (*Command, error) {
p.nextToken() // consume LOG
switch p.curToken.Type {
case TokenLevel:
return p.parseSetLogLevel()
default:
return nil, fmt.Errorf("unknown log target: %s", p.curToken.Value)
}
}
func (p *Parser) parseSetLogLevel() (*Command, error) {
p.nextToken() // consume LEVEL
cmd := NewCommand("set_log_level")
switch p.curToken.Type {
case TokenDebug:
cmd.Params["level"] = "debug"
case TokenInfo:
cmd.Params["level"] = "info"
case TokenWarn:
cmd.Params["level"] = "warn"
case TokenError:
cmd.Params["level"] = "error"
case TokenFatal:
cmd.Params["level"] = "fatal"
case TokenPanic:
cmd.Params["level"] = "panic"
default:
return nil, fmt.Errorf("unknown log target: %s", p.curToken.Value)
}
p.nextToken()
// Semicolon is optional for UNSET TOKEN
if p.curToken.Type == TokenSemicolon {
p.nextToken()
}
return cmd, nil
}
func (p *Parser) parseResetCommand() (*Command, error) {
p.nextToken() // consume RESET

View File

@ -165,6 +165,9 @@ func (h *SystemHandler) SetLogLevel(c *gin.Context) {
return
}
config := server.GetConfig()
config.Log.Level = req.Level
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "Log level updated successfully",

View File

@ -90,8 +90,6 @@ func (r *Router) Setup(engine *gin.Engine) {
engine.GET("/v1/system/config", r.systemHandler.GetConfig)
engine.GET("/v1/system/configs", r.systemHandler.GetConfigs)
engine.GET("/v1/system/version", r.systemHandler.GetVersion)
engine.GET("/v1/system/log_level", r.systemHandler.GetLogLevel)
engine.PUT("/v1/system/log_level", r.systemHandler.SetLogLevel)
engine.POST("/v1/user/register", r.userHandler.Register)
// User login channels endpoint
engine.GET("/v1/user/login/channels", r.userHandler.GetLoginChannels)
@ -136,12 +134,12 @@ func (r *Router) Setup(engine *gin.Engine) {
// users.GET("/:id", r.userHandler.GetUserByID)
//}
apiTokens := v1.Group("/tokens")
{
apiTokens.POST("", r.systemHandler.CreateToken)
apiTokens.GET("", r.systemHandler.ListTokens)
apiTokens.DELETE("/:token", r.systemHandler.DeleteToken)
}
//apiTokens := v1.Group("/tokens")
//{
// apiTokens.POST("", r.systemHandler.CreateToken)
// apiTokens.GET("", r.systemHandler.ListTokens)
// apiTokens.DELETE("/:token", r.systemHandler.DeleteToken)
//}
// Document routes
documents := v1.Group("/documents")
@ -233,6 +231,24 @@ func (r *Router) Setup(engine *gin.Engine) {
system := v1.Group("/system")
{
system.GET("/version", r.systemHandler.GetVersion)
system.GET("/configs", r.systemHandler.GetConfigs)
log := system.Group("/log")
{
// /api/v1/system/log GET
log.GET("", r.systemHandler.GetLogLevel)
// /api/v1/system/log PUT
log.PUT("", r.systemHandler.SetLogLevel)
}
tokens := system.Group("/tokens")
{
// list tokens /api/v1/system/tokens GET
tokens.GET("", r.systemHandler.ListTokens)
// create token /api/v1/system/tokens POST
tokens.POST("", r.systemHandler.CreateToken)
// delete token /api/v1/system/tokens/:token DELETE
tokens.DELETE("/:token", r.systemHandler.DeleteToken)
}
}
}