From 085241b039c753b35c93e297581a7a42524b2c2e Mon Sep 17 00:00:00 2001 From: tmimmanuel <14046872+tmimmanuel@users.noreply.github.com> Date: Wed, 27 May 2026 19:30:22 -1000 Subject: [PATCH] Go: implement system healthz API (#15307) ## Summary - Add Go REST support for `GET /api/v1/system/healthz`. - Return Python-compatible `ok`/`nok` dependency fields for DB, Redis, document engine, and storage. - Return HTTP 200 only when all checks pass; otherwise return HTTP 500 with `_meta` failure details. - Add focused service coverage for the unhealthy dependency response when Go dependencies are not initialized. ## Scope This is a small, isolated slice of #15240. It avoids current open connector PRs (#15274, #15300, #15265, #15264), tenant/member PRs (#15295, #15301, #15276), MCP PRs (#15281, #15253, #15254, #15260, #15261, #15262), and the memory-message PR (#15256). Refs #15240 --- internal/handler/system.go | 10 +++ internal/router/router.go | 1 + internal/service/system.go | 106 ++++++++++++++++++++++++++++++++ internal/service/system_test.go | 19 ++++++ 4 files changed, 136 insertions(+) create mode 100644 internal/service/system_test.go diff --git a/internal/handler/system.go b/internal/handler/system.go index 3a87f60dcf..66052df2b1 100644 --- a/internal/handler/system.go +++ b/internal/handler/system.go @@ -55,6 +55,16 @@ func (h *SystemHandler) Health(c *gin.Context) { }) } +// Healthz reports dependency health in the Python-compatible format. +func (h *SystemHandler) Healthz(c *gin.Context) { + result, allOK := h.systemService.Healthz(c.Request.Context()) + statusCode := http.StatusOK + if !allOK { + statusCode = http.StatusInternalServerError + } + c.JSON(statusCode, result) +} + // GetConfig get system configuration // @Summary Get System Configuration // @Description Get system configuration including register enabled status diff --git a/internal/router/router.go b/internal/router/router.go index 752f333871..191dc8dc7f 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -100,6 +100,7 @@ func (r *Router) Setup(engine *gin.Engine) { apiNoAuth.GET("/system/ping", r.systemHandler.Ping) apiNoAuth.GET("/system/config", r.systemHandler.GetConfig) apiNoAuth.GET("/system/version", r.systemHandler.GetVersion) + apiNoAuth.GET("/system/healthz", r.systemHandler.Healthz) // User login channels endpoint apiNoAuth.GET("/auth/login/channels", r.userHandler.GetLoginChannels) diff --git a/internal/service/system.go b/internal/service/system.go index bd0e1790fb..dc2bdb72bd 100644 --- a/internal/service/system.go +++ b/internal/service/system.go @@ -17,8 +17,15 @@ package service import ( + "context" + "fmt" + "ragflow/internal/cache" + "ragflow/internal/dao" + "ragflow/internal/engine" "ragflow/internal/server" + "ragflow/internal/storage" "ragflow/internal/utility" + "time" ) // SystemService system service @@ -53,6 +60,20 @@ type VersionResponse struct { Version string `json:"version"` } +type HealthzMeta struct { + Elapsed string `json:"elapsed"` + Error string `json:"error,omitempty"` +} + +type HealthzResponse struct { + DB string `json:"db"` + Redis string `json:"redis"` + DocEngine string `json:"doc_engine"` + Storage string `json:"storage"` + Status string `json:"status"` + Meta map[string]HealthzMeta `json:"_meta,omitempty"` +} + // GetVersion get RAGFlow version func (s *SystemService) GetVersion() (*VersionResponse, error) { version := utility.GetRAGFlowVersion() @@ -60,3 +81,88 @@ func (s *SystemService) GetVersion() (*VersionResponse, error) { Version: version, }, nil } + +func okNok(ok bool) string { + if ok { + return "ok" + } + return "nok" +} + +func timedHealthCheck(check func() error) (bool, HealthzMeta) { + start := time.Now() + err := check() + meta := HealthzMeta{ + Elapsed: fmt.Sprintf("%.1f", float64(time.Since(start).Microseconds())/1000.0), + } + if err != nil { + meta.Error = err.Error() + return false, meta + } + return true, meta +} + +// Healthz runs lightweight dependency checks for /api/v1/system/healthz. +func (s *SystemService) Healthz(ctx context.Context) (*HealthzResponse, bool) { + meta := map[string]HealthzMeta{} + + dbOK, dbMeta := timedHealthCheck(func() error { + if dao.DB == nil { + return fmt.Errorf("database is not initialized") + } + sqlDB, err := dao.DB.DB() + if err != nil { + return err + } + return sqlDB.PingContext(ctx) + }) + if !dbOK { + meta["db"] = dbMeta + } + + redisOK, redisMeta := timedHealthCheck(func() error { + redisClient := cache.Get() + if redisClient == nil || !redisClient.Health() { + return fmt.Errorf("redis is not healthy") + } + return nil + }) + if !redisOK { + meta["redis"] = redisMeta + } + + docOK, docMeta := timedHealthCheck(func() error { + docEngine := engine.Get() + if docEngine == nil { + return fmt.Errorf("document engine is not initialized") + } + return docEngine.Ping(ctx) + }) + if !docOK { + meta["doc_engine"] = docMeta + } + + storageOK, storageMeta := timedHealthCheck(func() error { + store := storage.GetStorageFactory().GetStorage() + if store == nil || !store.Health() { + return fmt.Errorf("storage is not healthy") + } + return nil + }) + if !storageOK { + meta["storage"] = storageMeta + } + + allOK := dbOK && redisOK && docOK && storageOK + result := &HealthzResponse{ + DB: okNok(dbOK), + Redis: okNok(redisOK), + DocEngine: okNok(docOK), + Storage: okNok(storageOK), + Status: okNok(allOK), + } + if len(meta) > 0 { + result.Meta = meta + } + return result, allOK +} diff --git a/internal/service/system_test.go b/internal/service/system_test.go new file mode 100644 index 0000000000..564b293f62 --- /dev/null +++ b/internal/service/system_test.go @@ -0,0 +1,19 @@ +package service + +import "testing" + +func TestSystemServiceHealthzReportsUnhealthyDependencies(t *testing.T) { + result, allOK := NewSystemService().Healthz(t.Context()) + if allOK { + t.Fatal("allOK=true, want false without initialized dependencies") + } + if result.Status != "nok" { + t.Fatalf("status=%q, want nok", result.Status) + } + if result.DB != "nok" || result.Redis != "nok" || result.DocEngine != "nok" || result.Storage != "nok" { + t.Fatalf("unexpected health result: %+v", result) + } + if len(result.Meta) == 0 { + t.Fatal("expected failure metadata") + } +}