mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-05-09 03:22:37 +02:00
Release prep: 54 engines, self-hosted signatures, i18n, dashboard updates
This commit is contained in:
parent
694e32be26
commit
41cbfd6e0a
178 changed files with 36008 additions and 399 deletions
281
internal/transport/http/resilience_handlers.go
Normal file
281
internal/transport/http/resilience_handlers.go
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/syntrex/gomcp/internal/application/resilience"
|
||||
)
|
||||
|
||||
// ResilienceAPI holds references to the SARL engines for HTTP handlers.
|
||||
type ResilienceAPI struct {
|
||||
healthMonitor *resilience.HealthMonitor
|
||||
healingEngine *resilience.HealingEngine
|
||||
preservation *resilience.PreservationEngine
|
||||
behavioral *resilience.BehavioralAnalyzer
|
||||
playbooks *resilience.RecoveryPlaybookEngine
|
||||
}
|
||||
|
||||
// NewResilienceAPI creates a new resilience API handler.
|
||||
// Any engine can be nil — the handler will return 503 for that subsystem.
|
||||
func NewResilienceAPI(
|
||||
hm *resilience.HealthMonitor,
|
||||
he *resilience.HealingEngine,
|
||||
pe *resilience.PreservationEngine,
|
||||
ba *resilience.BehavioralAnalyzer,
|
||||
pb *resilience.RecoveryPlaybookEngine,
|
||||
) *ResilienceAPI {
|
||||
return &ResilienceAPI{
|
||||
healthMonitor: hm,
|
||||
healingEngine: he,
|
||||
preservation: pe,
|
||||
behavioral: ba,
|
||||
playbooks: pb,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers all resilience API endpoints on the given mux.
|
||||
func (api *ResilienceAPI) RegisterRoutes(mux *http.ServeMux, rbac *RBACMiddleware) {
|
||||
// Read endpoints — viewer access.
|
||||
mux.HandleFunc("GET /api/v1/resilience/health",
|
||||
rbac.Require(RoleViewer, api.handleHealth))
|
||||
mux.HandleFunc("GET /api/v1/resilience/metrics/{component}",
|
||||
rbac.Require(RoleViewer, api.handleComponentMetrics))
|
||||
mux.HandleFunc("GET /api/v1/resilience/audit",
|
||||
rbac.Require(RoleAnalyst, api.handleAudit))
|
||||
mux.HandleFunc("GET /api/v1/resilience/healing/{id}",
|
||||
rbac.Require(RoleAnalyst, api.handleHealingStatus))
|
||||
|
||||
// Write endpoints — admin access.
|
||||
mux.HandleFunc("POST /api/v1/resilience/healing/initiate",
|
||||
rbac.Require(RoleAdmin, api.handleInitiateHealing))
|
||||
mux.HandleFunc("POST /api/v1/resilience/mode/activate",
|
||||
rbac.Require(RoleAdmin, api.handleActivateMode))
|
||||
}
|
||||
|
||||
// GET /api/v1/resilience/health
|
||||
func (api *ResilienceAPI) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
if api.healthMonitor == nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "health monitor not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
health := api.healthMonitor.GetHealth()
|
||||
|
||||
// Add emergency mode info from preservation engine.
|
||||
response := map[string]any{
|
||||
"overall_status": health.OverallStatus,
|
||||
"components": health.Components,
|
||||
"quorum_valid": health.QuorumValid,
|
||||
"last_check": health.LastCheck,
|
||||
"anomalies_detected": health.AnomaliesDetected,
|
||||
"active_emergency_mode": string(resilience.ModeNone),
|
||||
}
|
||||
|
||||
if api.preservation != nil {
|
||||
response["active_emergency_mode"] = string(api.preservation.CurrentMode())
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GET /api/v1/resilience/metrics/{component}
|
||||
func (api *ResilienceAPI) handleComponentMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
component := r.PathValue("component")
|
||||
if component == "" {
|
||||
writeError(w, http.StatusBadRequest, "missing component path parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if api.healthMonitor == nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "health monitor not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"component": component,
|
||||
"time_range": "1h",
|
||||
"status": "ok",
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/v1/resilience/audit
|
||||
func (api *ResilienceAPI) handleAudit(w http.ResponseWriter, r *http.Request) {
|
||||
var entries []any
|
||||
|
||||
// Combine healing operations + preservation events.
|
||||
if api.healingEngine != nil {
|
||||
ops := api.healingEngine.RecentOperations(50)
|
||||
for _, op := range ops {
|
||||
entries = append(entries, map[string]any{
|
||||
"type": "healing",
|
||||
"timestamp": op.StartedAt,
|
||||
"component": op.Component,
|
||||
"strategy": op.StrategyID,
|
||||
"result": op.Result,
|
||||
"error": op.Error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if api.preservation != nil {
|
||||
for _, evt := range api.preservation.History() {
|
||||
entries = append(entries, map[string]any{
|
||||
"type": "preservation",
|
||||
"timestamp": evt.Timestamp,
|
||||
"mode": evt.Mode,
|
||||
"action": evt.Action,
|
||||
"success": evt.Success,
|
||||
"error": evt.Error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if api.playbooks != nil {
|
||||
execs := api.playbooks.RecentExecutions(50)
|
||||
for _, exec := range execs {
|
||||
entries = append(entries, map[string]any{
|
||||
"type": "playbook",
|
||||
"timestamp": exec.StartedAt,
|
||||
"playbook": exec.PlaybookID,
|
||||
"component": exec.Component,
|
||||
"status": exec.Status,
|
||||
"error": exec.Error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"entries": entries,
|
||||
"total": len(entries),
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/v1/resilience/healing/{id}
|
||||
func (api *ResilienceAPI) handleHealingStatus(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
if id == "" {
|
||||
writeError(w, http.StatusBadRequest, "missing healing operation ID")
|
||||
return
|
||||
}
|
||||
|
||||
if api.healingEngine != nil {
|
||||
op, ok := api.healingEngine.GetOperation(id)
|
||||
if ok {
|
||||
writeJSON(w, http.StatusOK, op)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if api.playbooks != nil {
|
||||
exec, ok := api.playbooks.GetExecution(id)
|
||||
if ok {
|
||||
writeJSON(w, http.StatusOK, exec)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
writeError(w, http.StatusNotFound, "operation not found")
|
||||
}
|
||||
|
||||
// POST /api/v1/resilience/healing/initiate
|
||||
func (api *ResilienceAPI) handleInitiateHealing(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Component string `json:"component"`
|
||||
Strategy string `json:"strategy,omitempty"`
|
||||
Playbook string `json:"playbook,omitempty"`
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Component == "" {
|
||||
writeError(w, http.StatusBadRequest, "component is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Run playbook if specified.
|
||||
if req.Playbook != "" && api.playbooks != nil {
|
||||
execID, err := api.playbooks.Execute(r.Context(), req.Playbook, req.Component)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"healing_id": execID,
|
||||
"status": "FAILED",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"healing_id": execID,
|
||||
"status": "COMPLETED",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusAccepted, map[string]any{
|
||||
"component": req.Component,
|
||||
"status": "INITIATED",
|
||||
"message": "healing request queued",
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/resilience/mode/activate
|
||||
func (api *ResilienceAPI) handleActivateMode(w http.ResponseWriter, r *http.Request) {
|
||||
if api.preservation == nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "preservation engine not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Mode string `json:"mode"`
|
||||
Reason string `json:"reason"`
|
||||
Duration string `json:"duration,omitempty"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
var mode resilience.EmergencyMode
|
||||
switch strings.ToUpper(req.Mode) {
|
||||
case "SAFE":
|
||||
mode = resilience.ModeSafe
|
||||
case "LOCKDOWN":
|
||||
mode = resilience.ModeLockdown
|
||||
case "APOPTOSIS":
|
||||
mode = resilience.ModeApoptosis
|
||||
case "NONE", "":
|
||||
if err := api.preservation.DeactivateMode("api"); err != nil {
|
||||
writeError(w, http.StatusConflict, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"mode_activated": "NONE",
|
||||
"activated_at": time.Now(),
|
||||
})
|
||||
return
|
||||
default:
|
||||
writeError(w, http.StatusBadRequest, "invalid mode: "+req.Mode)
|
||||
return
|
||||
}
|
||||
|
||||
if err := api.preservation.ActivateMode(mode, req.Reason, "api"); err != nil {
|
||||
writeError(w, http.StatusConflict, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
activation := api.preservation.Activation()
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"mode_activated": string(mode),
|
||||
"activated_at": activation.ActivatedAt,
|
||||
"auto_exit_at": activation.AutoExitAt,
|
||||
})
|
||||
}
|
||||
|
||||
// writeJSON and writeJSONError are defined in server.go (shared across package).
|
||||
Loading…
Add table
Add a link
Reference in a new issue