Go: fix list services (#17664)

### Summary

1. Fix admin list services;
2. Fix admin show service 'service_name';

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
This commit is contained in:
Jin Hai
2026-08-01 14:52:37 +08:00
committed by GitHub
parent 776b9371f7
commit 26a5af8b83
19 changed files with 262 additions and 329 deletions

View File

@@ -490,7 +490,8 @@ func (h *Handler) DeleteUserAPIToken(c *gin.Context) {
// GetServices handle get all services
func (h *Handler) GetServices(c *gin.Context) {
services, err := h.service.ListServices()
ctx := c.Request.Context()
services, err := h.service.ListServices(ctx)
if err != nil {
common.ResponseWithHttpCodeData(c, http.StatusInternalServerError, common.CodeBadRequest, nil, err.Error())
return
@@ -518,48 +519,38 @@ func (h *Handler) GetServicesByType(c *gin.Context) {
// GetService handle get service details
func (h *Handler) GetService(c *gin.Context) {
serviceID := c.Param("service_id")
if serviceID == "" {
common.ErrorWithCode(c, common.CodeBadRequest, "Service ID is required")
serviceName := c.Param("service_name")
if serviceName == "" {
common.ErrorWithCode(c, common.CodeBadRequest, "Service name is required")
return
}
// Get all services and find the one with matching ID
allConfigs := server.GetAllConfigs()
var targetService map[string]interface{}
for _, config := range allConfigs {
if id, ok := config["id"]; ok {
if strconv.Itoa(id.(int)) == serviceID {
targetService = config
break
}
}
}
if targetService == nil {
common.ErrorWithCode(c, common.CodeNotFound, "Service not found")
return
}
serviceStatus, err := h.service.GetServiceDetails(targetService)
ctx := c.Request.Context()
services, err := h.service.ListServices(ctx)
if err != nil {
common.ErrorWithCode(c, common.CodeServerError, err.Error())
return
}
common.SuccessWithData(c, serviceStatus, "")
for _, serviceInstance := range services {
if serviceInstance.Name == serviceName {
common.SuccessWithData(c, serviceInstance, "")
return
}
}
common.ErrorWithCode(c, common.CodeNotFound, "Service not found")
}
// ShutdownService handle shutdown service
func (h *Handler) ShutdownService(c *gin.Context) {
serviceID := c.Param("service_id")
if serviceID == "" {
common.ErrorWithCode(c, common.CodeBadRequest, "Service ID is required")
serviceName := c.Param("service_name")
if serviceName == "" {
common.ErrorWithCode(c, common.CodeBadRequest, "Service name is required")
return
}
result, err := h.service.ShutdownService(serviceID)
result, err := h.service.ShutdownService(serviceName)
if err != nil {
common.ErrorWithCode(c, common.CodeServerError, err.Error())
return
@@ -570,13 +561,13 @@ func (h *Handler) ShutdownService(c *gin.Context) {
// StartService handle start service
func (h *Handler) StartService(c *gin.Context) {
serviceID := c.Param("service_id")
if serviceID == "" {
common.ErrorWithCode(c, common.CodeBadRequest, "Service ID is required")
serviceName := c.Param("service_name")
if serviceName == "" {
common.ErrorWithCode(c, common.CodeBadRequest, "Service name is required")
return
}
result, err := h.service.StartService(serviceID)
result, err := h.service.StartService(serviceName)
if err != nil {
common.ErrorWithCode(c, common.CodeServerError, err.Error())
return
@@ -587,13 +578,13 @@ func (h *Handler) StartService(c *gin.Context) {
// RestartService handle restart service
func (h *Handler) RestartService(c *gin.Context) {
serviceID := c.Param("service_id")
if serviceID == "" {
common.ErrorWithCode(c, common.CodeBadRequest, "Service ID is required")
serviceName := c.Param("service_name")
if serviceName == "" {
common.ErrorWithCode(c, common.CodeBadRequest, "Service name is required")
return
}
result, err := h.service.RestartService(serviceID)
result, err := h.service.RestartService(serviceName)
if err != nil {
common.ErrorWithCode(c, common.CodeServerError, err.Error())
return

View File

@@ -75,10 +75,10 @@ func (r *Router) Setup(engine *gin.Engine) {
// Service management
protected.GET("/services", r.handler.GetServices)
protected.GET("/service_types/:service_type", r.handler.GetServicesByType)
protected.GET("/services/:service_id", r.handler.GetService)
protected.DELETE("/services/:service_id", r.handler.ShutdownService)
protected.PUT("/services/:service_id", r.handler.RestartService)
protected.POST("/services/:service_id", r.handler.StartService)
protected.GET("/services/:service_name", r.handler.GetService)
protected.DELETE("/services/:service_name", r.handler.ShutdownService)
protected.PUT("/services/:service_name", r.handler.RestartService)
protected.POST("/services/:service_name", r.handler.StartService)
// Variables/Settings
protected.GET("/variables", r.handler.ListVariables)

View File

@@ -19,7 +19,6 @@ package admin
import (
"context"
"crypto/rand"
"crypto/tls"
"encoding/base64"
"encoding/hex"
"errors"
@@ -36,9 +35,9 @@ import (
"ragflow/internal/server"
"ragflow/internal/server/config"
servicepkg "ragflow/internal/service"
"ragflow/internal/storage"
"ragflow/internal/utility"
"regexp"
"strconv"
"time"
"go.uber.org/zap"
@@ -963,60 +962,88 @@ func (s *Service) DeleteUserAPIToken(ctx context.Context, username, key string)
return nil
}
// ListServices get all services
func (s *Service) ListServices() ([]map[string]interface{}, error) {
allConfigs := server.GetAllConfigs()
type ServiceStatus struct {
Type string `json:"type"`
Name string `json:"name"`
Status string `json:"status"`
Elapsed string `json:"elapsed"`
Message string `json:"message"`
}
var results []map[string]interface{}
for _, configDict := range allConfigs {
serviceType := configDict["service_type"]
if serviceType != "ragflow_server" {
// Get service details to check status
serviceDetail, err := s.GetServiceDetails(configDict)
if err == nil {
if status, ok := serviceDetail["status"]; ok {
configDict["status"] = status
} else {
configDict["status"] = "timeout"
}
} else {
configDict["status"] = "timeout"
}
if serviceDetail != nil {
// delete element of configDict
delete(configDict, "extra")
delete(configDict, "database")
delete(configDict, "password")
delete(configDict, "sample_ratio")
delete(configDict, "secure")
delete(configDict, "stdout")
delete(configDict, "user")
results = append(results, configDict)
}
}
func newServiceStatus(typeStr, nameStr, statusStr string, startTime time.Time, messageStr string) ServiceStatus {
return ServiceStatus{
Type: typeStr,
Name: nameStr,
Status: statusStr,
Elapsed: fmt.Sprintf("%.1d", time.Since(startTime).Milliseconds()),
Message: messageStr,
}
}
// ListServices get all services
func (s *Service) ListServices(ctx context.Context) ([]ServiceStatus, error) {
var results []ServiceStatus
globalConfig := server.GetConfig()
// Database
databaseType := globalConfig.DatabaseType()
switch databaseType {
case "mysql":
mysqlStatus := s.getMySQLStatus(ctx)
results = append(results, mysqlStatus)
default:
results = append(results, newServiceStatus("database", databaseType, "not available", time.Now(), "not supported database type"))
}
// Doc engine
docEngineImpl := engine.Get()
err := docEngineImpl.Ping(ctx)
if err == nil {
results = append(results, newServiceStatus("doc_engine", docEngineImpl.GetType(), "alive", time.Now(), ""))
} else {
results = append(results, newServiceStatus("doc_engine", docEngineImpl.GetType(), "timeout", time.Now(), err.Error()))
}
// storage engine
storageImpl := storage.GetStorageFactory().GetStorage()
storageHealth := storageImpl.Health()
if storageHealth {
results = append(results, newServiceStatus("storage_engine", storageImpl.Type(), "alive", time.Now(), ""))
} else {
results = append(results, newServiceStatus("storage_engine", storageImpl.Type(), "timeout", time.Now(), ""))
}
// cache engine
cacheType := globalConfig.CacheEngineType()
switch cacheType {
case "redis":
mysqlStatus := s.getRedisInfo(ctx)
results = append(results, mysqlStatus)
default:
results = append(results, newServiceStatus("database", databaseType, "not available", time.Now(), "not supported database type"))
}
// message queue
messageQueueImpl := engine.GetMessageQueueEngine()
messageQueueStatus := messageQueueImpl.CheckStatus()
results = append(results, newServiceStatus("message_queue", messageQueueImpl.Type(), messageQueueStatus, time.Now(), ""))
results = append(results, s.GetEEServicesStatus()...)
serverList := GlobalServerStore.ListInfos()
now := time.Now()
for _, serverStatus := range serverList {
serverItem := make(map[string]interface{})
serverItem["name"] = serverStatus.ServerName
serverItem["service_type"] = serverStatus.ServerType
serverItem["host"] = serverStatus.Host
serverItem["port"] = serverStatus.Port
now := time.Now()
serverItem := newServiceStatus(string(serverStatus.ServerType), serverStatus.ServerName, "", serverStatus.Timestamp, "")
// the difference between now and serverStatus.Timestamp is less than 5 seconds, then the server is alive
if now.Sub(serverStatus.Timestamp) < 30*time.Second {
serverItem["status"] = "alive"
if now.Sub(serverStatus.Timestamp) < 45*time.Second {
serverItem.Status = "alive"
} else {
serverItem["status"] = "timeout"
serverItem.Status = "timeout"
}
results = append(results, serverItem)
}
for id, result := range results {
result["id"] = id
}
return results, nil
}
@@ -1026,169 +1053,143 @@ func (s *Service) GetServicesByType(serviceType string) ([]map[string]interface{
}
// GetServiceDetails get service details
func (s *Service) GetServiceDetails(configDict map[string]interface{}) (map[string]interface{}, error) {
serviceType, _ := configDict["service_type"].(string)
name, _ := configDict["name"].(string)
// Call detail function based on service type
switch serviceType {
case "meta_data":
return s.getMySQLStatus(name)
case "cache":
return s.getRedisInfo(name)
case "message_queue":
host := configDict["host"].(string)
port := configDict["port"].(int)
return s.checkNatsAlive(name, host, port)
case "retrieval":
// Check the extra.retrieval_type to determine which retrieval service
if extra, ok := configDict["extra"].(map[string]interface{}); ok {
if retrievalType, ok := extra["retrieval_type"].(string); ok {
if retrievalType == "infinity" {
return s.getInfinityStatus(name)
}
}
}
return s.getESClusterStats(name)
case "ragflow_server":
return s.checkRAGFlowServerAlive(name)
case "file_store":
return s.checkMinioAlive(name)
case "olap":
return s.checkOlapAlive(name)
case "tracing":
return s.checkTracingAlive(name)
default:
return nil, nil
}
func (s *Service) GetServiceDetails(configDict map[string]interface{}) ([]ServiceStatus, error) {
//serviceType, _ := configDict["service_type"].(string)
//name, _ := configDict["name"].(string)
//ctx := context.Background()
//
//// Call detail function based on service type
//switch serviceType {
//case "meta_data":
// return s.getMySQLStatus(ctx), nil
//case "cache":
// return s.getRedisInfo(ctx), nil
//case "message_queue":
// host := configDict["host"].(string)
// port := configDict["port"].(int)
// return s.checkNatsAlive(name, host, port)
//case "retrieval":
// // Check the extra.retrieval_type to determine which retrieval service
// if extra, ok := configDict["extra"].(map[string]interface{}); ok {
// if retrievalType, ok := extra["retrieval_type"].(string); ok {
// if retrievalType == "infinity" {
// return s.getInfinityStatus(name)
// }
// }
// }
// return s.getESClusterStats(name)
//case "ragflow_server":
// return s.checkRAGFlowServerAlive(name)
//case "file_store":
// return s.checkMinioAlive(name)
//case "olap":
// return s.checkOlapAlive(name)
//case "tracing":
// return s.checkTracingAlive(name)
//default:
// return nil, nil
//}
return nil, nil
}
// getMySQLStatus gets MySQL service status
func (s *Service) getMySQLStatus(name string) (map[string]interface{}, error) {
func (s *Service) getMySQLStatus(ctx context.Context) ServiceStatus {
serviceType := "database"
name := "mysql"
startTime := time.Now()
// Check basic connectivity with SELECT 1
sqlDB, err := dao.DB.DB()
if err != nil {
return map[string]interface{}{
"service_name": name,
"status": "timeout",
"elapsed": fmt.Sprintf("%.1d", time.Since(startTime).Milliseconds()),
"message": err.Error(),
}, nil
return newServiceStatus(serviceType, name, "not connected", startTime, err.Error())
}
// Execute SELECT 1 to check connectivity
_, err = sqlDB.Exec("SELECT 1")
err = sqlDB.PingContext(ctx)
if err != nil {
return map[string]interface{}{
"service_name": name,
"status": "timeout",
"elapsed": fmt.Sprintf("%.1d", time.Since(startTime).Milliseconds()),
"message": err.Error(),
}, nil
return newServiceStatus(serviceType, name, "timeout", startTime, err.Error())
}
return map[string]interface{}{
"service_name": name,
"status": "alive",
"elapsed": fmt.Sprintf("%.1d", time.Since(startTime).Milliseconds()),
"message": "MySQL connection successful",
}, nil
return newServiceStatus(serviceType, name, "alive", startTime, "")
}
// getRedisInfo gets Redis service info
func (s *Service) getRedisInfo(name string) (map[string]interface{}, error) {
func (s *Service) getRedisInfo(ctx context.Context) ServiceStatus {
serviceType := "cache"
name := "redis"
startTime := time.Now()
redisClient := redis.Get()
if redisClient == nil {
return map[string]interface{}{
"service_name": name,
"status": "timeout",
"elapsed": fmt.Sprintf("%.1d", time.Since(startTime).Milliseconds()),
"error": "Redis client not initialized",
}, nil
if redisClient.Health() {
return newServiceStatus(serviceType, name, "alive", startTime, "")
}
// Check health
if !redisClient.Health() {
return map[string]interface{}{
"service_name": name,
"status": "timeout",
"elapsed": fmt.Sprintf("%.1d", time.Since(startTime).Milliseconds()),
"error": "Redis health check failed",
}, nil
}
return map[string]interface{}{
"service_name": name,
"status": "alive",
"elapsed": fmt.Sprintf("%.1d", time.Since(startTime).Milliseconds()),
"message": "Redis connection successful",
}, nil
return newServiceStatus(serviceType, name, "timeout", startTime, "Redis health check failed")
}
// getESClusterStats gets Elasticsearch cluster stats
func (s *Service) getESClusterStats(name string) (map[string]interface{}, error) {
// Check if Elasticsearch is the doc engine
docEngine := common.GetEnv(common.EnvDocEngine)
if docEngine == "" {
docEngine = "elasticsearch"
}
if docEngine != "elasticsearch" {
return map[string]interface{}{
"service_name": name,
"status": "timeout",
"message": "error: Elasticsearch is not in use.",
}, nil
}
func (s *Service) getESClusterStats(serviceType string) map[string]interface{} {
name := "elasticsearch"
startTime := time.Now()
// Get ES config from server config
cfg := server.GetConfig()
if cfg == nil || !cfg.IsElasticConfigured() {
return map[string]interface{}{
"service_name": name,
"status": "timeout",
"message": "error: Elasticsearch configuration not found",
}, nil
"type": serviceType,
"name": name,
"status": "timeout",
"elapsed": fmt.Sprintf("%.1d", time.Since(startTime).Milliseconds()),
"message": "error: Elasticsearch configuration not found",
}
}
// Create ES engine and get cluster stats
esEngine, err := elasticsearch.NewEngine(cfg.GetElasticsearchConfig())
if err != nil {
return map[string]interface{}{
"service_name": name,
"status": "timeout",
"message": fmt.Sprintf("error: %s", err.Error()),
}, nil
"type": serviceType,
"name": name,
"status": "timeout",
"elapsed": fmt.Sprintf("%.1d", time.Since(startTime).Milliseconds()),
"message": fmt.Sprintf("error: %s", err.Error()),
}
}
defer esEngine.Close()
clusterStats, err := esEngine.GetClusterStats()
if err != nil {
return map[string]interface{}{
"service_name": name,
"status": "timeout",
"message": fmt.Sprintf("error: %s", err.Error()),
}, nil
"type": serviceType,
"name": name,
"status": "timeout",
"elapsed": fmt.Sprintf("%.1d", time.Since(startTime).Milliseconds()),
"message": fmt.Sprintf("error: %s", err.Error()),
}
}
return map[string]interface{}{
"service_name": name,
"status": "alive",
"message": clusterStats,
}, nil
"type": serviceType,
"name": name,
"status": "alive",
"elapsed": fmt.Sprintf("%.1d", time.Since(startTime).Milliseconds()),
"message": clusterStats,
}
}
// getInfinityStatus gets Infinity service status
func (s *Service) getInfinityStatus(name string) (map[string]interface{}, error) {
func (s *Service) getInfinityStatus(serviceType string) map[string]interface{} {
name := "infinity"
startTime := time.Now()
// TODO: Implement actual Infinity health check
return map[string]interface{}{
"service_name": name,
"status": "unknown",
"message": "Infinity health check not implemented",
}, nil
"type": serviceType,
"name": name,
"status": "unknown",
"elapsed": fmt.Sprintf("%.1d", time.Since(startTime).Milliseconds()),
"message": "Infinity health check not implemented",
}
}
// checkRAGFlowServerAlive checks if RAGFlow server is alive
@@ -1257,104 +1258,6 @@ func (s *Service) checkRAGFlowServerAlive(name string) (map[string]interface{},
}, nil
}
// checkMinioAlive checks if MinIO is alive
func (s *Service) checkMinioAlive(name string) (map[string]interface{}, error) {
startTime := time.Now()
// Get MinIO file store config from allConfigs
var host string
var port int
var secure bool
verify := true
allConfigs := server.GetAllConfigs()
for _, config := range allConfigs {
if serviceType, ok := config["service_type"].(string); ok && serviceType == "file_store" {
// Get host from config
if h, ok := config["host"].(string); ok {
host = h
}
if p, ok := config["port"].(int); ok {
port = p
} else if p, ok := config["port"].(float64); ok {
port = int(p)
} else if p, ok := config["port"].(string); ok {
if parsedPort, err := strconv.Atoi(p); err == nil {
port = parsedPort
}
}
// Get secure from extra config
if extra, ok := config["extra"].(map[string]interface{}); ok {
if s, ok := extra["secure"].(bool); ok {
secure = s
} else if s, ok := extra["secure"].(string); ok {
secure = s == "true" || s == "1" || s == "yes"
}
if v, ok := extra["verify"].(bool); ok {
verify = v
} else if v, ok := extra["verify"].(string); ok {
verify = !(v == "false" || v == "0" || v == "no")
}
}
break
}
}
// Default host
if host == "" {
host = "localhost"
}
if port == 0 {
port = 9000
}
// Determine scheme
scheme := "http"
if secure {
scheme = "https"
}
url := fmt.Sprintf("%s://%s:%d/minio/health/live", scheme, host, port)
// Create HTTP client with timeout
client := &http.Client{
Timeout: 10 * time.Second,
}
// If verify is false, we need to skip SSL verification
if !verify && scheme == "https" {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
resp, err := client.Get(url)
if err != nil {
return map[string]interface{}{
"service_name": name,
"status": "timeout",
"message": fmt.Sprintf("error: %s", err.Error()),
}, nil
}
defer resp.Body.Close()
elapsed := time.Since(startTime).Milliseconds()
if resp.StatusCode == 200 {
return map[string]interface{}{
"service_name": name,
"status": "alive",
"message": fmt.Sprintf("Confirm elapsed: %.1f ms.", float64(elapsed)),
}, nil
}
return map[string]interface{}{
"service_name": name,
"status": "timeout",
"message": fmt.Sprintf("Confirm elapsed: %.1f ms.", float64(elapsed)),
}, nil
}
// checkTaskExecutorAlive checks if task executor is alive
func (s *Service) checkTaskExecutorAlive(name string) (map[string]interface{}, error) {
// TODO: Implement actual task executor health check
@@ -1420,30 +1323,30 @@ func (s *Service) checkOlapAlive(name string) (map[string]interface{}, error) {
}
// ShutdownService shutdown service
func (s *Service) ShutdownService(serviceID string) (map[string]interface{}, error) {
func (s *Service) ShutdownService(serviceName string) (map[string]interface{}, error) {
// TODO: Implement with proper service manager
return map[string]interface{}{
"command": "shutdown service",
"service_id": serviceID,
"error": "shutdown service not implemented",
"command": "shutdown service",
"service_name": serviceName,
"error": "shutdown service not implemented",
}, nil
}
// StartService start service
func (s *Service) StartService(serviceID string) (map[string]interface{}, error) {
func (s *Service) StartService(serviceName string) (map[string]interface{}, error) {
return map[string]interface{}{
"command": "start service",
"service_id": serviceID,
"error": "command 'start service' isn't implemented",
"command": "start service",
"service_name": serviceName,
"error": "command 'start service' isn't implemented",
}, nil
}
// RestartService restart service
func (s *Service) RestartService(serviceID string) (map[string]interface{}, error) {
func (s *Service) RestartService(serviceName string) (map[string]interface{}, error) {
return map[string]interface{}{
"command": "restart service",
"service_id": serviceID,
"error": "command 'restart service' isn't implemented",
"command": "restart service",
"service_name": serviceName,
"error": "command 'restart service' isn't implemented",
}, nil
}

View File

@@ -1351,3 +1351,7 @@ func (s *Service) ListLogs(ctx context.Context, userName string, days int) ([]ma
}
return result, nil
}
func (s *Service) GetEEServicesStatus() []ServiceStatus {
return []ServiceStatus{}
}