Add AI service dashboard endpoint
This commit is contained in:
85
internal/httpapi/dashboard.go
Normal file
85
internal/httpapi/dashboard.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package httpapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"ai-service/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dashboardResponse struct {
|
||||||
|
At time.Time `json:"at"`
|
||||||
|
Summary dashboardSummary `json:"summary"`
|
||||||
|
Stats *model.Stats `json:"stats"`
|
||||||
|
Providers providersStatusResponse `json:"providers"`
|
||||||
|
Infra infraStatusResponse `json:"infra"`
|
||||||
|
Jobs []*model.Job `json:"jobs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dashboardSummary struct {
|
||||||
|
Pending int64 `json:"pending"`
|
||||||
|
Running int64 `json:"running"`
|
||||||
|
Done int64 `json:"done"`
|
||||||
|
Failed int64 `json:"failed"`
|
||||||
|
Cancelled int64 `json:"cancelled"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
ctx, cancel := contextWithTimeout(r, 12*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
stats, err := s.store.Stats(ctx)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jobs, err := s.store.ListJobs(ctx, model.JobFilter{
|
||||||
|
Statuses: []string{model.StatusFailed, model.StatusRunning},
|
||||||
|
Limit: 40,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := dashboardResponse{
|
||||||
|
At: now,
|
||||||
|
Summary: summarizeQueues(stats),
|
||||||
|
Stats: stats,
|
||||||
|
Providers: providersStatusResponse{
|
||||||
|
At: now,
|
||||||
|
Providers: []providerStatus{
|
||||||
|
s.checkLLM(ctx),
|
||||||
|
s.checkWhisperX(ctx),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Infra: loadInfraSnapshot(r, s.cfg),
|
||||||
|
Jobs: jobs,
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func summarizeQueues(stats *model.Stats) dashboardSummary {
|
||||||
|
var out dashboardSummary
|
||||||
|
if stats == nil {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
for _, row := range stats.Queues {
|
||||||
|
switch row.Status {
|
||||||
|
case model.StatusPending:
|
||||||
|
out.Pending += row.Total
|
||||||
|
case model.StatusRunning:
|
||||||
|
out.Running += row.Total
|
||||||
|
case model.StatusDone:
|
||||||
|
out.Done += row.Total
|
||||||
|
case model.StatusFailed:
|
||||||
|
out.Failed += row.Total
|
||||||
|
case model.StatusCancelled:
|
||||||
|
out.Cancelled += row.Total
|
||||||
|
}
|
||||||
|
out.Total += row.Total
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"ai-service/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type infraStatusResponse struct {
|
type infraStatusResponse struct {
|
||||||
@@ -17,15 +19,18 @@ type infraStatusResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleInfraStatus(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleInfraStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
resp := infraStatusResponse{At: time.Now().UTC()}
|
writeJSON(w, http.StatusOK, loadInfraSnapshot(r, s.cfg))
|
||||||
baseURL := strings.TrimRight(strings.TrimSpace(s.cfg.AIStatsSidecarURL), "/")
|
|
||||||
if baseURL == "" {
|
|
||||||
resp.SidecarError = "AI stats sidecar is not configured"
|
|
||||||
writeJSON(w, http.StatusOK, resp)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
timeout := s.cfg.AIStatsTimeout
|
func loadInfraSnapshot(r *http.Request, cfg config.Config) infraStatusResponse {
|
||||||
|
resp := infraStatusResponse{At: time.Now().UTC()}
|
||||||
|
baseURL := strings.TrimRight(strings.TrimSpace(cfg.AIStatsSidecarURL), "/")
|
||||||
|
if baseURL == "" {
|
||||||
|
resp.SidecarError = "AI stats sidecar is not configured"
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := cfg.AIStatsTimeout
|
||||||
if timeout <= 0 {
|
if timeout <= 0 {
|
||||||
timeout = 8 * time.Second
|
timeout = 8 * time.Second
|
||||||
}
|
}
|
||||||
@@ -37,7 +42,7 @@ func (s *Server) handleInfraStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
} else {
|
} else {
|
||||||
resp.Sidecar = sidecar
|
resp.Sidecar = sidecar
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, resp)
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAIStatsSidecar(ctx context.Context, baseURL string, timeout time.Duration) (map[string]any, error) {
|
func fetchAIStatsSidecar(ctx context.Context, baseURL string, timeout time.Duration) (map[string]any, error) {
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.handleProviderStatus(w, r)
|
s.handleProviderStatus(w, r)
|
||||||
case r.Method == http.MethodGet && path == "/api/v1/infra/status":
|
case r.Method == http.MethodGet && path == "/api/v1/infra/status":
|
||||||
s.handleInfraStatus(w, r)
|
s.handleInfraStatus(w, r)
|
||||||
|
case r.Method == http.MethodGet && path == "/api/v1/dashboard":
|
||||||
|
s.handleDashboard(w, r)
|
||||||
default:
|
default:
|
||||||
writeError(w, http.StatusNotFound, "not found")
|
writeError(w, http.StatusNotFound, "not found")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user