feat(routing): add route health admin view
This commit is contained in:
@@ -51,6 +51,7 @@ type ActionSet struct {
|
||||
ListRouteFailoverEvents func(context.Context, ListRouteFailoverEventsRequest) ([]RouteFailoverEventInfo, error)
|
||||
AppendRouteStickyAudit func(context.Context, AppendRouteStickyAuditRequest) (RouteStickyAuditInfo, error)
|
||||
ListRouteStickyAudit func(context.Context, ListRouteStickyAuditRequest) ([]RouteStickyAuditInfo, error)
|
||||
ListRouteHealth func(context.Context, ListRouteHealthRequest) ([]RouteHealthInfo, error)
|
||||
ResolveRoute func(context.Context, ResolveRouteRequest) (ResolveRouteInfo, error)
|
||||
RouteChatCompletions func(context.Context, RouteChatCompletionsRequest) (RouteChatCompletionsResult, error)
|
||||
ProxyRouteChatCompletions func(context.Context, ProxyRouteChatCompletionsRequest) (ProxyRouteChatCompletionsResult, error)
|
||||
@@ -401,6 +402,9 @@ func NewAPIHandlerWithAuth(adminAuth AdminAuthConfig, actions ActionSet) http.Ha
|
||||
mux.Handle("GET /api/routing/logs/sticky-audit", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handleListRouteStickyAudit(w, r, actions.ListRouteStickyAudit)
|
||||
})))
|
||||
mux.Handle("GET /api/routing/routes/health", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handleListRouteHealth(w, r, actions.ListRouteHealth)
|
||||
})))
|
||||
mux.Handle("POST /api/routing/resolve", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handleResolveRoute(w, r, actions.ResolveRoute)
|
||||
})))
|
||||
@@ -1266,6 +1270,7 @@ func NewActionSetWithStickyRuntime(sqliteDSN string, stickyRuntime stickyStoreRu
|
||||
ListRouteFailoverEvents: buildListRouteFailoverEventsAction(sqliteDSN),
|
||||
AppendRouteStickyAudit: buildAppendRouteStickyAuditAction(routeLogWriter, sqliteDSN),
|
||||
ListRouteStickyAudit: buildListRouteStickyAuditAction(sqliteDSN),
|
||||
ListRouteHealth: buildListRouteHealthAction(sqliteDSN, stickyRuntime),
|
||||
ResolveRoute: resolveRoute,
|
||||
RouteChatCompletions: routeChatCompletions,
|
||||
ProxyRouteChatCompletions: proxyRouteChatCompletions,
|
||||
|
||||
295
internal/app/route_health_api.go
Normal file
295
internal/app/route_health_api.go
Normal file
@@ -0,0 +1,295 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"sub2api-cn-relay-manager/internal/routing"
|
||||
"sub2api-cn-relay-manager/internal/store/sqlite"
|
||||
)
|
||||
|
||||
const (
|
||||
routeRuntimeStatusHealthy = "healthy"
|
||||
routeRuntimeStatusCooldown = "cooldown"
|
||||
routeRuntimeStatusFailing = "failing"
|
||||
routeRuntimeStatusDisabled = "disabled"
|
||||
|
||||
defaultRouteHealthFailoverLimit = 20
|
||||
)
|
||||
|
||||
type ListRouteHealthRequest struct {
|
||||
LogicalGroupID string
|
||||
RouteID string
|
||||
Status string
|
||||
}
|
||||
|
||||
type RouteHealthInfo struct {
|
||||
Backend string `json:"backend"`
|
||||
RouteID string `json:"route_id"`
|
||||
RouteName string `json:"route_name,omitempty"`
|
||||
LogicalGroupID string `json:"logical_group_id"`
|
||||
LogicalGroupDisplayName string `json:"logical_group_display_name,omitempty"`
|
||||
LogicalGroupStatus string `json:"logical_group_status,omitempty"`
|
||||
ConfiguredStatus string `json:"configured_status,omitempty"`
|
||||
ShadowHostID string `json:"shadow_host_id"`
|
||||
ShadowGroupID string `json:"shadow_group_id"`
|
||||
Priority int `json:"priority"`
|
||||
Weight int `json:"weight,omitempty"`
|
||||
RuntimeStatus string `json:"runtime_status"`
|
||||
FailureCount int `json:"failure_count"`
|
||||
CooldownUntil string `json:"cooldown_until,omitempty"`
|
||||
CooldownReason string `json:"cooldown_reason,omitempty"`
|
||||
LastErrorClass string `json:"last_error_class,omitempty"`
|
||||
LastSelectedAt string `json:"last_selected_at,omitempty"`
|
||||
LastUpstreamStatus int `json:"last_upstream_status,omitempty"`
|
||||
LastRequestID string `json:"last_request_id,omitempty"`
|
||||
LastPublicModel string `json:"last_public_model,omitempty"`
|
||||
RecentFailoverCount int `json:"recent_failover_count"`
|
||||
UpstreamBaseURLHint string `json:"upstream_base_url_hint,omitempty"`
|
||||
UpdatedAt string `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
func handleListRouteHealth(w http.ResponseWriter, r *http.Request, fn func(context.Context, ListRouteHealthRequest) ([]RouteHealthInfo, error)) {
|
||||
if fn == nil {
|
||||
writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "list-route-health action is not configured"})
|
||||
return
|
||||
}
|
||||
req, err := decodeListRouteHealthRequest(r)
|
||||
if err != nil {
|
||||
writeHTTPError(w, err)
|
||||
return
|
||||
}
|
||||
items, actionErr := fn(r.Context(), req)
|
||||
if actionErr != nil {
|
||||
writeHTTPError(w, classifyError(actionErr))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"route_health": items})
|
||||
}
|
||||
|
||||
func buildListRouteHealthAction(sqliteDSN string, stickyRuntime stickyStoreRuntime) func(context.Context, ListRouteHealthRequest) ([]RouteHealthInfo, error) {
|
||||
return func(ctx context.Context, req ListRouteHealthRequest) ([]RouteHealthInfo, error) {
|
||||
store, err := sqlite.Open(ctx, sqliteDSN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
groups, routes, err := loadRouteHealthScope(ctx, store, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]RouteHealthInfo, 0, len(routes))
|
||||
for _, route := range routes {
|
||||
group, ok := groups[route.LogicalGroupID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("logical group %q not found for route %q", route.LogicalGroupID, route.RouteID)
|
||||
}
|
||||
item, err := buildRouteHealthInfo(ctx, store, stickyRuntime, group, route)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.Status != "" && !strings.EqualFold(item.RuntimeStatus, req.Status) {
|
||||
continue
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
}
|
||||
|
||||
func decodeListRouteHealthRequest(r *http.Request) (ListRouteHealthRequest, *httpError) {
|
||||
status := strings.TrimSpace(r.URL.Query().Get("status"))
|
||||
if status != "" && !isSupportedRouteHealthStatus(status) {
|
||||
return ListRouteHealthRequest{}, &httpError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Code: "bad_request",
|
||||
Message: fmt.Sprintf("unsupported route health status %q", status),
|
||||
}
|
||||
}
|
||||
return ListRouteHealthRequest{
|
||||
LogicalGroupID: strings.TrimSpace(r.URL.Query().Get("logical_group_id")),
|
||||
RouteID: strings.TrimSpace(r.URL.Query().Get("route_id")),
|
||||
Status: strings.ToLower(status),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadRouteHealthScope(ctx context.Context, store *sqlite.DB, req ListRouteHealthRequest) (map[string]sqlite.LogicalGroup, []sqlite.LogicalGroupRoute, error) {
|
||||
if req.RouteID != "" {
|
||||
route, err := store.LogicalGroupRoutes().GetByRouteID(ctx, req.RouteID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return map[string]sqlite.LogicalGroup{}, nil, nil
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
if req.LogicalGroupID != "" && route.LogicalGroupID != req.LogicalGroupID {
|
||||
return map[string]sqlite.LogicalGroup{}, nil, nil
|
||||
}
|
||||
group, err := store.LogicalGroups().GetByLogicalGroupID(ctx, route.LogicalGroupID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return map[string]sqlite.LogicalGroup{group.LogicalGroupID: group}, []sqlite.LogicalGroupRoute{route}, nil
|
||||
}
|
||||
|
||||
if req.LogicalGroupID != "" {
|
||||
group, err := store.LogicalGroups().GetByLogicalGroupID(ctx, req.LogicalGroupID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
routes, err := store.LogicalGroupRoutes().ListByLogicalGroupID(ctx, req.LogicalGroupID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return map[string]sqlite.LogicalGroup{group.LogicalGroupID: group}, routes, nil
|
||||
}
|
||||
|
||||
groupRows, err := store.LogicalGroups().List(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
groupMap := make(map[string]sqlite.LogicalGroup, len(groupRows))
|
||||
routes := make([]sqlite.LogicalGroupRoute, 0)
|
||||
for _, group := range groupRows {
|
||||
groupMap[group.LogicalGroupID] = group
|
||||
groupRoutes, err := store.LogicalGroupRoutes().ListByLogicalGroupID(ctx, group.LogicalGroupID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
routes = append(routes, groupRoutes...)
|
||||
}
|
||||
return groupMap, routes, nil
|
||||
}
|
||||
|
||||
func buildRouteHealthInfo(ctx context.Context, store *sqlite.DB, stickyRuntime stickyStoreRuntime, group sqlite.LogicalGroup, route sqlite.LogicalGroupRoute) (RouteHealthInfo, error) {
|
||||
failureState, hasFailure, err := stickyRuntime.store.GetRouteFailure(ctx, route.RouteID)
|
||||
if err != nil {
|
||||
return RouteHealthInfo{}, err
|
||||
}
|
||||
cooldownState, hasCooldown, err := stickyRuntime.store.GetCooldown(ctx, route.RouteID)
|
||||
if err != nil {
|
||||
return RouteHealthInfo{}, err
|
||||
}
|
||||
|
||||
// 兼容 route 表上的手工 cooldown 标记,避免健康页漏掉已配置的禁用窗口。
|
||||
if !hasCooldown && !routeExitsCooldown(route.CooldownUntil) {
|
||||
hasCooldown = true
|
||||
cooldownState = routing.RouteCooldownState{
|
||||
RouteID: route.RouteID,
|
||||
Reason: "configured_cooldown",
|
||||
Until: route.CooldownUntil,
|
||||
}
|
||||
}
|
||||
|
||||
decisionRows, err := store.RouteDecisionLogs().ListRecent(ctx, sqlite.RouteDecisionLogFilter{
|
||||
SelectedRouteID: route.RouteID,
|
||||
Limit: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return RouteHealthInfo{}, err
|
||||
}
|
||||
|
||||
failoverFrom, err := store.RouteFailoverEvents().ListRecent(ctx, sqlite.RouteFailoverEventFilter{
|
||||
FromRouteID: route.RouteID,
|
||||
Limit: defaultRouteHealthFailoverLimit,
|
||||
})
|
||||
if err != nil {
|
||||
return RouteHealthInfo{}, err
|
||||
}
|
||||
failoverTo, err := store.RouteFailoverEvents().ListRecent(ctx, sqlite.RouteFailoverEventFilter{
|
||||
ToRouteID: route.RouteID,
|
||||
Limit: defaultRouteHealthFailoverLimit,
|
||||
})
|
||||
if err != nil {
|
||||
return RouteHealthInfo{}, err
|
||||
}
|
||||
|
||||
item := RouteHealthInfo{
|
||||
Backend: stickyRuntime.backend,
|
||||
RouteID: route.RouteID,
|
||||
RouteName: route.Name,
|
||||
LogicalGroupID: route.LogicalGroupID,
|
||||
LogicalGroupDisplayName: group.DisplayName,
|
||||
LogicalGroupStatus: group.Status,
|
||||
ConfiguredStatus: route.Status,
|
||||
ShadowHostID: route.ShadowHostID,
|
||||
ShadowGroupID: route.ShadowGroupID,
|
||||
Priority: route.Priority,
|
||||
Weight: route.Weight,
|
||||
RuntimeStatus: deriveRouteRuntimeStatus(group.Status, route.Status, hasCooldown, failureCountFromState(failureState, hasFailure)),
|
||||
FailureCount: failureCountFromState(failureState, hasFailure),
|
||||
CooldownUntil: cooldownUntilFromState(cooldownState, hasCooldown),
|
||||
CooldownReason: cooldownReasonFromState(cooldownState, hasCooldown),
|
||||
LastErrorClass: lastErrorClassFromStates(failureState, hasFailure, decisionRows),
|
||||
RecentFailoverCount: len(failoverFrom) + len(failoverTo),
|
||||
UpstreamBaseURLHint: route.UpstreamBaseURLHint,
|
||||
UpdatedAt: route.UpdatedAt,
|
||||
}
|
||||
|
||||
if len(decisionRows) > 0 {
|
||||
item.LastSelectedAt = decisionRows[0].CreatedAt
|
||||
item.LastUpstreamStatus = decisionRows[0].UpstreamStatus
|
||||
item.LastRequestID = decisionRows[0].RequestID
|
||||
item.LastPublicModel = decisionRows[0].PublicModel
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func deriveRouteRuntimeStatus(groupStatus, routeStatus string, hasCooldown bool, failureCount int) string {
|
||||
if !isActiveStatus(groupStatus) || !isActiveStatus(routeStatus) {
|
||||
return routeRuntimeStatusDisabled
|
||||
}
|
||||
if hasCooldown {
|
||||
return routeRuntimeStatusCooldown
|
||||
}
|
||||
if failureCount > 0 {
|
||||
return routeRuntimeStatusFailing
|
||||
}
|
||||
return routeRuntimeStatusHealthy
|
||||
}
|
||||
|
||||
func failureCountFromState(state routing.RouteFailureState, ok bool) int {
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return state.FailureCount
|
||||
}
|
||||
|
||||
func cooldownUntilFromState(state routing.RouteCooldownState, ok bool) string {
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(state.Until)
|
||||
}
|
||||
|
||||
func cooldownReasonFromState(state routing.RouteCooldownState, ok bool) string {
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(state.Reason)
|
||||
}
|
||||
|
||||
func lastErrorClassFromStates(state routing.RouteFailureState, hasFailure bool, decisions []sqlite.RouteDecisionLog) string {
|
||||
if hasFailure && strings.TrimSpace(state.LastErrorClass) != "" {
|
||||
return strings.TrimSpace(state.LastErrorClass)
|
||||
}
|
||||
if len(decisions) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(decisions[0].ErrorClass)
|
||||
}
|
||||
|
||||
func isSupportedRouteHealthStatus(status string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(status)) {
|
||||
case routeRuntimeStatusHealthy, routeRuntimeStatusCooldown, routeRuntimeStatusFailing, routeRuntimeStatusDisabled:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
160
internal/app/route_health_api_test.go
Normal file
160
internal/app/route_health_api_test.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAPIListRouteHealthRejectsInvalidStatus(t *testing.T) {
|
||||
handler := NewAPIHandler("secret-token", ActionSet{
|
||||
ListRouteHealth: func(context.Context, ListRouteHealthRequest) ([]RouteHealthInfo, error) {
|
||||
t.Fatal("ListRouteHealth should not be called")
|
||||
return nil, nil
|
||||
},
|
||||
})
|
||||
|
||||
request := httptestRequest(t, http.MethodGet, "/api/routing/routes/health?status=broken", nil, "secret-token")
|
||||
response := httptestRecorder(handler, request)
|
||||
assertStatusCode(t, response, http.StatusBadRequest)
|
||||
assertJSONContains(t, response.Body().Bytes(), "error.code", "bad_request")
|
||||
}
|
||||
|
||||
func TestNewActionSetRouteHealthFlow(t *testing.T) {
|
||||
dsn := "file:" + filepath.ToSlash(filepath.Join(t.TempDir(), "route-health.db")) + "?_busy_timeout=5000"
|
||||
actions := NewActionSet(dsn)
|
||||
ctx := context.Background()
|
||||
|
||||
if _, err := actions.CreateLogicalGroup(ctx, CreateLogicalGroupRequest{
|
||||
LogicalGroupID: "gpt-shared",
|
||||
DisplayName: "GPT Shared",
|
||||
Status: "active",
|
||||
RoutePolicy: "priority",
|
||||
StickyMode: "conversation_preferred",
|
||||
ConversationTTLSeconds: 1200,
|
||||
UserModelTTLSeconds: 600,
|
||||
FailoverThreshold: 2,
|
||||
CooldownSeconds: 300,
|
||||
}); err != nil {
|
||||
t.Fatalf("CreateLogicalGroup(gpt-shared) error = %v", err)
|
||||
}
|
||||
|
||||
createRoute := func(routeID, name, status, shadowGroup string, priority int) {
|
||||
t.Helper()
|
||||
if _, err := actions.CreateLogicalGroupRoute(ctx, CreateLogicalGroupRouteRequest{
|
||||
LogicalGroupID: "gpt-shared",
|
||||
RouteID: routeID,
|
||||
Name: name,
|
||||
Status: status,
|
||||
Priority: priority,
|
||||
ShadowGroupID: shadowGroup,
|
||||
ShadowHostID: "remote43",
|
||||
UpstreamBaseURLHint: "https://example.com/v1",
|
||||
}); err != nil {
|
||||
t.Fatalf("CreateLogicalGroupRoute(%s) error = %v", routeID, err)
|
||||
}
|
||||
}
|
||||
|
||||
createRoute("healthy-route", "Healthy", "active", "shadow-healthy", 10)
|
||||
createRoute("failing-route", "Failing", "active", "shadow-failing", 20)
|
||||
createRoute("cooldown-route", "Cooldown", "active", "shadow-cooldown", 30)
|
||||
createRoute("disabled-route", "Disabled", "disabled", "shadow-disabled", 40)
|
||||
|
||||
if _, err := actions.AppendRouteDecisionLog(ctx, AppendRouteDecisionLogRequest{
|
||||
RequestID: "req-healthy",
|
||||
LogicalGroupID: "gpt-shared",
|
||||
PublicModel: "gpt-5.4",
|
||||
SelectedRouteID: "healthy-route",
|
||||
SelectedShadowGroupID: "shadow-healthy",
|
||||
UpstreamStatus: 200,
|
||||
Sync: true,
|
||||
}); err != nil {
|
||||
t.Fatalf("AppendRouteDecisionLog(healthy) error = %v", err)
|
||||
}
|
||||
|
||||
if _, err := actions.AppendRouteDecisionLog(ctx, AppendRouteDecisionLogRequest{
|
||||
RequestID: "req-failing",
|
||||
LogicalGroupID: "gpt-shared",
|
||||
PublicModel: "gpt-5.4",
|
||||
SelectedRouteID: "failing-route",
|
||||
SelectedShadowGroupID: "shadow-failing",
|
||||
ErrorClass: "upstream_5xx",
|
||||
UpstreamStatus: 503,
|
||||
Sync: true,
|
||||
}); err != nil {
|
||||
t.Fatalf("AppendRouteDecisionLog(failing) error = %v", err)
|
||||
}
|
||||
|
||||
if _, err := actions.AppendRouteFailoverEvent(ctx, AppendRouteFailoverEventRequest{
|
||||
RequestID: "req-failover",
|
||||
LogicalGroupID: "gpt-shared",
|
||||
PublicModel: "gpt-5.4",
|
||||
FromRouteID: "failing-route",
|
||||
ToRouteID: "healthy-route",
|
||||
Reason: "failure_threshold_exceeded:timeout",
|
||||
FailureCount: 2,
|
||||
Sync: true,
|
||||
}); err != nil {
|
||||
t.Fatalf("AppendRouteFailoverEvent() error = %v", err)
|
||||
}
|
||||
|
||||
if _, err := actions.SetRouteFailure(ctx, SetRouteFailureRequest{
|
||||
RouteID: "failing-route",
|
||||
FailureCount: 2,
|
||||
LastErrorClass: "timeout",
|
||||
TTLSeconds: 600,
|
||||
}); err != nil {
|
||||
t.Fatalf("SetRouteFailure() error = %v", err)
|
||||
}
|
||||
|
||||
if _, err := actions.SetRouteCooldown(ctx, SetRouteCooldownRequest{
|
||||
RouteID: "cooldown-route",
|
||||
Reason: "degraded",
|
||||
TTLSeconds: 600,
|
||||
}); err != nil {
|
||||
t.Fatalf("SetRouteCooldown() error = %v", err)
|
||||
}
|
||||
|
||||
items, err := actions.ListRouteHealth(ctx, ListRouteHealthRequest{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListRouteHealth() error = %v", err)
|
||||
}
|
||||
if len(items) != 4 {
|
||||
t.Fatalf("ListRouteHealth() len = %d, want 4", len(items))
|
||||
}
|
||||
|
||||
byRoute := make(map[string]RouteHealthInfo, len(items))
|
||||
for _, item := range items {
|
||||
byRoute[item.RouteID] = item
|
||||
}
|
||||
|
||||
if got := byRoute["healthy-route"]; got.RuntimeStatus != routeRuntimeStatusHealthy || got.LastUpstreamStatus != 200 {
|
||||
t.Fatalf("healthy-route = %+v, want healthy with upstream 200", got)
|
||||
}
|
||||
if got := byRoute["failing-route"]; got.RuntimeStatus != routeRuntimeStatusFailing || got.FailureCount != 2 || got.LastErrorClass != "timeout" || got.RecentFailoverCount != 1 || got.LastUpstreamStatus != 503 {
|
||||
t.Fatalf("failing-route = %+v, want failing with failure_count=2 recent_failover_count=1 upstream=503", got)
|
||||
}
|
||||
if got := byRoute["cooldown-route"]; got.RuntimeStatus != routeRuntimeStatusCooldown || got.CooldownReason != "degraded" || got.CooldownUntil == "" {
|
||||
t.Fatalf("cooldown-route = %+v, want cooldown with reason degraded", got)
|
||||
}
|
||||
if got := byRoute["disabled-route"]; got.RuntimeStatus != routeRuntimeStatusDisabled {
|
||||
t.Fatalf("disabled-route = %+v, want disabled", got)
|
||||
}
|
||||
|
||||
failingOnly, err := actions.ListRouteHealth(ctx, ListRouteHealthRequest{Status: routeRuntimeStatusFailing})
|
||||
if err != nil {
|
||||
t.Fatalf("ListRouteHealth(failing) error = %v", err)
|
||||
}
|
||||
if len(failingOnly) != 1 || failingOnly[0].RouteID != "failing-route" {
|
||||
t.Fatalf("ListRouteHealth(failing) = %+v, want only failing-route", failingOnly)
|
||||
}
|
||||
|
||||
oneRoute, err := actions.ListRouteHealth(ctx, ListRouteHealthRequest{RouteID: "cooldown-route"})
|
||||
if err != nil {
|
||||
t.Fatalf("ListRouteHealth(route_id) error = %v", err)
|
||||
}
|
||||
if len(oneRoute) != 1 || oneRoute[0].RouteID != "cooldown-route" {
|
||||
t.Fatalf("ListRouteHealth(route_id) = %+v, want only cooldown-route", oneRoute)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user