mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
c253495045
Change-source: pulse-maintainer
258 lines
7.7 KiB
Go
258 lines
7.7 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
type stateSummaryResponse struct {
|
|
ActiveAlerts int `json:"activeAlerts"`
|
|
Nodes int `json:"nodes"`
|
|
VMs int `json:"vms"`
|
|
Containers int `json:"containers"`
|
|
DockerHosts []stateSummaryDockerHost `json:"dockerHosts"`
|
|
LastUpdate time.Time `json:"lastUpdate"`
|
|
Verdicts stateSummaryVerdicts `json:"verdicts"`
|
|
Attention []stateSummaryAttention `json:"attention"`
|
|
}
|
|
|
|
type stateSummaryVerdicts struct {
|
|
OK int `json:"ok"`
|
|
Attention int `json:"attention"`
|
|
Critical int `json:"critical"`
|
|
Stale int `json:"stale"`
|
|
Off int `json:"off"`
|
|
Unknown int `json:"unknown"`
|
|
}
|
|
|
|
type stateSummaryAttention struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Type unifiedresources.ResourceType `json:"type"`
|
|
PlatformType string `json:"platformType,omitempty"`
|
|
Verdict unifiedresources.ResourceHealthVerdict `json:"verdict"`
|
|
TopReason *unifiedresources.ResourceHealthReason `json:"topReason,omitempty"`
|
|
}
|
|
|
|
type stateSummaryDockerHost struct {
|
|
Name string `json:"name"`
|
|
Containers int `json:"containers"`
|
|
UptimeSeconds int64 `json:"uptimeSeconds"`
|
|
CPUUsagePercent float64 `json:"cpuUsagePercent"`
|
|
}
|
|
|
|
// Keep the additive summary useful without allowing its attention list to
|
|
// approach the size of the full resource response. Thirty representative
|
|
// entries stay below the endpoint's 5 KiB target with ordinary identifiers.
|
|
const stateSummaryAttentionLimit = 30
|
|
|
|
func buildStateSummary(
|
|
readState unifiedresources.ReadState,
|
|
resources []unifiedresources.Resource,
|
|
activeAlerts []models.Alert,
|
|
lastUpdate time.Time,
|
|
) stateSummaryResponse {
|
|
postureResources := unifiedresources.AttachResourceHealth(
|
|
resources,
|
|
stateSummaryHealthAlerts(activeAlerts),
|
|
time.Now().UTC(),
|
|
)
|
|
verdicts, attention := summarizeResourceHealth(postureResources)
|
|
if readState == nil {
|
|
return stateSummaryResponse{
|
|
ActiveAlerts: len(activeAlerts),
|
|
LastUpdate: lastUpdate,
|
|
Verdicts: verdicts,
|
|
Attention: attention,
|
|
}
|
|
}
|
|
|
|
dockerHosts := make([]stateSummaryDockerHost, 0, len(readState.DockerHosts()))
|
|
for _, host := range readState.DockerHosts() {
|
|
if host == nil {
|
|
continue
|
|
}
|
|
|
|
name := strings.TrimSpace(host.CustomDisplayName())
|
|
if name == "" {
|
|
name = strings.TrimSpace(host.DisplayName())
|
|
}
|
|
if name == "" {
|
|
name = strings.TrimSpace(host.Hostname())
|
|
}
|
|
if name == "" {
|
|
name = host.ID()
|
|
}
|
|
|
|
dockerHosts = append(dockerHosts, stateSummaryDockerHost{
|
|
Name: name,
|
|
Containers: len(host.Containers()),
|
|
UptimeSeconds: host.UptimeSeconds(),
|
|
CPUUsagePercent: host.CPUPercent(),
|
|
})
|
|
}
|
|
|
|
return stateSummaryResponse{
|
|
ActiveAlerts: len(activeAlerts),
|
|
Nodes: len(readState.Nodes()),
|
|
VMs: len(readState.VMs()),
|
|
Containers: len(readState.Containers()),
|
|
DockerHosts: dockerHosts,
|
|
LastUpdate: lastUpdate,
|
|
Verdicts: verdicts,
|
|
Attention: attention,
|
|
}
|
|
}
|
|
|
|
func stateSummaryHealthAlerts(active []models.Alert) []unifiedresources.ResourceHealthAlert {
|
|
out := make([]unifiedresources.ResourceHealthAlert, 0, len(active))
|
|
for _, alert := range active {
|
|
out = append(out, unifiedresources.ResourceHealthAlert{
|
|
ResourceID: alert.ResourceID,
|
|
Level: alert.Level,
|
|
Type: alert.Type,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func summarizeResourceHealth(resources []unifiedresources.Resource) (stateSummaryVerdicts, []stateSummaryAttention) {
|
|
counts := stateSummaryVerdicts{}
|
|
attention := make([]stateSummaryAttention, 0)
|
|
for _, resource := range resources {
|
|
if resource.Health == nil {
|
|
continue
|
|
}
|
|
switch resource.Health.Verdict {
|
|
case unifiedresources.HealthOK:
|
|
counts.OK++
|
|
case unifiedresources.HealthAttention:
|
|
counts.Attention++
|
|
case unifiedresources.HealthCritical:
|
|
counts.Critical++
|
|
case unifiedresources.HealthStale:
|
|
counts.Stale++
|
|
case unifiedresources.HealthOff:
|
|
counts.Off++
|
|
case unifiedresources.HealthUnknown:
|
|
counts.Unknown++
|
|
}
|
|
if resource.Health.Verdict == unifiedresources.HealthOK || resource.Health.Verdict == unifiedresources.HealthOff {
|
|
continue
|
|
}
|
|
name := strings.TrimSpace(resource.Name)
|
|
if resource.Canonical != nil && strings.TrimSpace(resource.Canonical.DisplayName) != "" {
|
|
name = strings.TrimSpace(resource.Canonical.DisplayName)
|
|
}
|
|
entry := stateSummaryAttention{
|
|
ID: resource.ID, Name: name, Type: unifiedresources.ContractResourceType(resource),
|
|
PlatformType: stateSummaryPlatformType(resource), Verdict: resource.Health.Verdict,
|
|
}
|
|
if len(resource.Health.Reasons) > 0 {
|
|
reason := resource.Health.Reasons[0]
|
|
entry.TopReason = &reason
|
|
}
|
|
attention = append(attention, entry)
|
|
}
|
|
sort.SliceStable(attention, func(i, j int) bool {
|
|
left, right := summaryVerdictRank(attention[i].Verdict), summaryVerdictRank(attention[j].Verdict)
|
|
if left != right {
|
|
return left > right
|
|
}
|
|
return strings.ToLower(attention[i].Name) < strings.ToLower(attention[j].Name)
|
|
})
|
|
if len(attention) > stateSummaryAttentionLimit {
|
|
attention = attention[:stateSummaryAttentionLimit]
|
|
}
|
|
if attention == nil {
|
|
attention = []stateSummaryAttention{}
|
|
}
|
|
return counts, attention
|
|
}
|
|
|
|
func summaryVerdictRank(verdict unifiedresources.ResourceHealthVerdict) int {
|
|
switch verdict {
|
|
case unifiedresources.HealthCritical:
|
|
return 4
|
|
case unifiedresources.HealthAttention:
|
|
return 3
|
|
case unifiedresources.HealthStale:
|
|
return 2
|
|
case unifiedresources.HealthUnknown:
|
|
return 1
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func stateSummaryPlatformType(resource unifiedresources.Resource) string {
|
|
if resource.Proxmox != nil || resource.PBS != nil || resource.PMG != nil {
|
|
return "proxmox"
|
|
}
|
|
if resource.Docker != nil {
|
|
return "docker"
|
|
}
|
|
if resource.Kubernetes != nil {
|
|
return "kubernetes"
|
|
}
|
|
if resource.TrueNAS != nil {
|
|
return "truenas"
|
|
}
|
|
if resource.VMware != nil {
|
|
return "vmware"
|
|
}
|
|
if resource.Availability != nil {
|
|
return "availability"
|
|
}
|
|
if len(resource.Sources) > 0 {
|
|
return string(resource.Sources[0])
|
|
}
|
|
return "generic"
|
|
}
|
|
|
|
func (r *Router) handleStateSummary(w http.ResponseWriter, req *http.Request) {
|
|
if req.Method != http.MethodGet {
|
|
writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed",
|
|
"Only GET method is allowed", nil)
|
|
return
|
|
}
|
|
|
|
authWriter := &responseCapture{ResponseWriter: w}
|
|
if !checkAuth(r.config, authWriter, req, false) {
|
|
if !authWriter.wrote {
|
|
writeErrorResponse(w, http.StatusUnauthorized, "unauthorized",
|
|
"Authentication required", nil)
|
|
}
|
|
return
|
|
}
|
|
|
|
if record := getAPITokenRecordFromRequest(req); record != nil && !record.HasScope(config.ScopeMonitoringRead) {
|
|
respondMissingScope(w, config.ScopeMonitoringRead)
|
|
return
|
|
}
|
|
|
|
monitor := r.getTenantMonitor(req.Context())
|
|
if monitor == nil {
|
|
writeErrorResponse(w, http.StatusInternalServerError, "no_monitor",
|
|
"Monitor not available", nil)
|
|
return
|
|
}
|
|
|
|
readState := monitor.GetUnifiedReadStateOrSnapshot()
|
|
snapshot := monitor.ReadSnapshot()
|
|
resources, _ := monitor.UnifiedResourceSnapshot()
|
|
if err := utils.WriteJSONResponse(w, buildStateSummary(readState, resources, snapshot.ActiveAlerts, snapshot.LastUpdate)); err != nil {
|
|
log.Error().Err(err).Msg("Failed to encode state summary response")
|
|
writeErrorResponse(w, http.StatusInternalServerError, "encoding_error",
|
|
"Failed to encode state summary", nil)
|
|
}
|
|
}
|