Add analytics metrics and failure ingestion
This commit is contained in:
145
internal/api/failures.go
Normal file
145
internal/api/failures.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"reanimator/internal/domain"
|
||||
"reanimator/internal/repository/failures"
|
||||
"reanimator/internal/repository/registry"
|
||||
)
|
||||
|
||||
type FailureDependencies struct {
|
||||
Failures *failures.FailureRepository
|
||||
Components *registry.ComponentRepository
|
||||
Assets *registry.AssetRepository
|
||||
}
|
||||
|
||||
type failureHandlers struct {
|
||||
deps FailureDependencies
|
||||
}
|
||||
|
||||
func RegisterFailureRoutes(mux *http.ServeMux, deps FailureDependencies) {
|
||||
h := failureHandlers{deps: deps}
|
||||
mux.HandleFunc("/ingest/failures", h.handleFailureIngest)
|
||||
}
|
||||
|
||||
type failureIngestRequest struct {
|
||||
Source string `json:"source"`
|
||||
Failures []failureIngestItem `json:"failures"`
|
||||
}
|
||||
|
||||
type failureIngestItem struct {
|
||||
ExternalID string `json:"external_id"`
|
||||
ComponentID int64 `json:"component_id"`
|
||||
AssetID *int64 `json:"asset_id"`
|
||||
FailureType string `json:"failure_type"`
|
||||
FailureTime string `json:"failure_time"`
|
||||
Details *string `json:"details"`
|
||||
Confidence *float64 `json:"confidence"`
|
||||
}
|
||||
|
||||
func (h failureHandlers) handleFailureIngest(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if h.deps.Failures == nil {
|
||||
writeError(w, http.StatusInternalServerError, "failures unavailable")
|
||||
return
|
||||
}
|
||||
|
||||
var req failureIngestRequest
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Source) == "" {
|
||||
writeError(w, http.StatusBadRequest, "source is required")
|
||||
return
|
||||
}
|
||||
if len(req.Failures) == 0 {
|
||||
writeError(w, http.StatusBadRequest, "failures is required")
|
||||
return
|
||||
}
|
||||
|
||||
tx, err := h.deps.Failures.BeginTx(r.Context())
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failure ingest failed")
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
for _, item := range req.Failures {
|
||||
if strings.TrimSpace(item.ExternalID) == "" {
|
||||
writeError(w, http.StatusBadRequest, "failures.external_id is required")
|
||||
return
|
||||
}
|
||||
if item.ComponentID <= 0 {
|
||||
writeError(w, http.StatusBadRequest, "failures.component_id is required")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(item.FailureType) == "" {
|
||||
writeError(w, http.StatusBadRequest, "failures.failure_type is required")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(item.FailureTime) == "" {
|
||||
writeError(w, http.StatusBadRequest, "failures.failure_time is required")
|
||||
return
|
||||
}
|
||||
|
||||
if h.deps.Components != nil {
|
||||
if _, err := h.deps.Components.Get(r.Context(), item.ComponentID); err != nil {
|
||||
if err == registry.ErrNotFound {
|
||||
writeError(w, http.StatusBadRequest, "component_id not found")
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, "component lookup failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
if item.AssetID != nil && h.deps.Assets != nil {
|
||||
if _, err := h.deps.Assets.Get(r.Context(), *item.AssetID); err != nil {
|
||||
if err == registry.ErrNotFound {
|
||||
writeError(w, http.StatusBadRequest, "asset_id not found")
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, "asset lookup failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
failureTime, err := time.Parse(time.RFC3339, item.FailureTime)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "failures.failure_time must be RFC3339")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = h.deps.Failures.Upsert(r.Context(), tx, domain.FailureEvent{
|
||||
Source: strings.TrimSpace(req.Source),
|
||||
ExternalID: strings.TrimSpace(item.ExternalID),
|
||||
ComponentID: item.ComponentID,
|
||||
AssetID: item.AssetID,
|
||||
FailureType: strings.TrimSpace(item.FailureType),
|
||||
FailureTime: failureTime,
|
||||
Details: item.Details,
|
||||
Confidence: item.Confidence,
|
||||
})
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failure ingest failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failure ingest failed")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"synced": len(req.Failures),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user