Pulse/internal/api/monitored_system_ledger.go
2026-03-24 11:46:54 +00:00

276 lines
8.7 KiB
Go

package api
import (
"encoding/json"
"net/http"
"time"
"github.com/rs/zerolog/log"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
// MonitoredSystemLedgerEntry represents a single counted top-level monitored
// system.
type MonitoredSystemLedgerEntry struct {
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"` // "online", "warning", "offline", "unknown"
StatusExplanation MonitoredSystemLedgerStatusExplanation `json:"status_explanation"`
LatestIncludedSignal MonitoredSystemLedgerLatestSignal `json:"latest_included_signal"`
Source string `json:"source"`
Explanation MonitoredSystemLedgerExplanation `json:"explanation"`
}
type MonitoredSystemLedgerLatestSignal struct {
Name string `json:"name"`
Type string `json:"type"`
Source string `json:"source"`
At string `json:"at"`
}
type MonitoredSystemLedgerStatusExplanation struct {
Summary string `json:"summary"`
Reasons []MonitoredSystemLedgerStatusReason `json:"reasons"`
}
type MonitoredSystemLedgerStatusReason struct {
Kind string `json:"kind"`
Name string `json:"name"`
Type string `json:"type"`
Source string `json:"source"`
Status string `json:"status"`
ReportedAt string `json:"reported_at"`
Summary string `json:"summary"`
}
type MonitoredSystemLedgerExplanation struct {
Summary string `json:"summary"`
Reasons []MonitoredSystemLedgerExplanationReason `json:"reasons"`
Surfaces []MonitoredSystemLedgerExplanationSurface `json:"surfaces"`
}
type MonitoredSystemLedgerExplanationReason struct {
Kind string `json:"kind"`
Signal string `json:"signal"`
Summary string `json:"summary"`
}
type MonitoredSystemLedgerExplanationSurface struct {
Name string `json:"name"`
Type string `json:"type"`
Source string `json:"source"`
}
// MonitoredSystemLedgerResponse is the response for GET /api/license/monitored-system-ledger.
type MonitoredSystemLedgerResponse struct {
Systems []MonitoredSystemLedgerEntry `json:"systems"`
Total int `json:"total"`
Limit int `json:"limit"` // 0 = unlimited
}
func EmptyMonitoredSystemLedgerResponse() MonitoredSystemLedgerResponse {
return MonitoredSystemLedgerResponse{}.NormalizeCollections()
}
func (r MonitoredSystemLedgerResponse) NormalizeCollections() MonitoredSystemLedgerResponse {
if r.Systems == nil {
r.Systems = []MonitoredSystemLedgerEntry{}
}
for i := range r.Systems {
r.Systems[i] = r.Systems[i].NormalizeCollections()
}
return r
}
func (e MonitoredSystemLedgerEntry) NormalizeCollections() MonitoredSystemLedgerEntry {
if e.LatestIncludedSignal.Name == "" {
e.LatestIncludedSignal.Name = "Unnamed source"
}
if e.LatestIncludedSignal.Type == "" {
e.LatestIncludedSignal.Type = "system"
}
if e.StatusExplanation.Reasons == nil {
e.StatusExplanation.Reasons = []MonitoredSystemLedgerStatusReason{}
}
if e.Explanation.Reasons == nil {
e.Explanation.Reasons = []MonitoredSystemLedgerExplanationReason{}
}
if e.Explanation.Surfaces == nil {
e.Explanation.Surfaces = []MonitoredSystemLedgerExplanationSurface{}
}
return e
}
func (r *Router) handleMonitoredSystemLedger(w http.ResponseWriter, req *http.Request) {
orgID := GetOrgID(req.Context())
// Get canonical monitored systems from the unified ReadState surface.
var systems []unifiedresources.MonitoredSystemRecord
var monitorResolved bool
if r.mtMonitor != nil {
monitor, monErr := r.mtMonitor.GetMonitor(orgID)
if monErr != nil {
log.Warn().Err(monErr).Str("org", orgID).Msg("monitored-system-ledger: failed to resolve tenant monitor")
}
if monitor != nil {
if rs := monitor.GetUnifiedReadState(); rs != nil {
systems = unifiedresources.MonitoredSystems(rs)
}
monitorResolved = true
}
}
// Fallback to the default monitor only for the default org to avoid cross-tenant data leaks.
if !monitorResolved && orgID == "default" && r.monitor != nil {
if rs := r.monitor.GetUnifiedReadState(); rs != nil {
systems = unifiedresources.MonitoredSystems(rs)
}
}
entries := make([]MonitoredSystemLedgerEntry, 0, len(systems))
for _, system := range systems {
entries = append(entries, monitoredSystemLedgerEntry(system))
}
limit := maxMonitoredSystemsLimitForContext(req.Context())
resp := EmptyMonitoredSystemLedgerResponse()
resp.Systems = entries
resp.Total = len(entries)
resp.Limit = limit
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp.NormalizeCollections())
}
func monitoredSystemLedgerEntry(system unifiedresources.MonitoredSystemRecord) MonitoredSystemLedgerEntry {
status := normalizeStatus(string(system.Status))
latestIncludedSignal := monitoredSystemLedgerLatestSignal(system.LatestIncludedSignal)
return MonitoredSystemLedgerEntry{
Name: system.Name,
Type: system.Type,
Status: status,
StatusExplanation: monitoredSystemLedgerStatusExplanation(system.StatusExplanation, status),
LatestIncludedSignal: latestIncludedSignal,
Source: system.Source,
Explanation: monitoredSystemLedgerExplanation(system.Explanation),
}
}
// ---------------------------------------------------------------------------
// Status helpers
// ---------------------------------------------------------------------------
func normalizeStatus(s string) string {
switch s {
case "online", "warning", "offline", "unknown":
return s
default:
return "unknown"
}
}
func monitoredSystemLedgerStatusExplanation(
explanation unifiedresources.MonitoredSystemStatusExplanation,
status string,
) MonitoredSystemLedgerStatusExplanation {
reasons := make([]MonitoredSystemLedgerStatusReason, 0, len(explanation.Reasons))
for _, reason := range explanation.Reasons {
reasons = append(reasons, MonitoredSystemLedgerStatusReason{
Kind: reason.Kind,
Name: reason.Name,
Type: reason.Type,
Source: reason.Source,
Status: normalizeMonitoredSystemLedgerReasonStatus(reason.Status),
ReportedAt: formatMonitoredSystemTime(reason.ReportedAt),
Summary: reason.Summary,
})
}
summary := explanation.Summary
if summary == "" {
summary = defaultMonitoredSystemLedgerStatusSummary(status)
}
return MonitoredSystemLedgerStatusExplanation{
Summary: summary,
Reasons: reasons,
}
}
func defaultMonitoredSystemLedgerStatusSummary(status string) string {
switch status {
case "online":
return "All included top-level collection paths currently report online status."
case "warning":
return "At least one included top-level collection path is degraded, so Pulse marks this monitored system as warning."
case "offline":
return "At least one included source is offline or disconnected, so Pulse marks this monitored system as offline."
default:
return "Pulse cannot determine a canonical runtime status for this monitored system yet."
}
}
func normalizeMonitoredSystemLedgerReasonStatus(status string) string {
switch status {
case "online", "stale", "offline", "unknown":
return status
default:
return "unknown"
}
}
func normalizeMonitoredSystemLedgerSource(source string) string {
switch source {
case "agent", "docker", "kubernetes", "pbs", "pmg", "proxmox", "truenas":
return source
default:
return ""
}
}
func monitoredSystemLedgerLatestSignal(
signal unifiedresources.MonitoredSystemLatestSignal,
) MonitoredSystemLedgerLatestSignal {
return MonitoredSystemLedgerLatestSignal{
Name: signal.Name,
Type: signal.Type,
Source: normalizeMonitoredSystemLedgerSource(signal.Source),
At: formatMonitoredSystemTime(signal.At),
}
}
func formatMonitoredSystemTime(t time.Time) string {
if t.IsZero() {
return ""
}
return t.UTC().Format(time.RFC3339)
}
func monitoredSystemLedgerExplanation(
explanation unifiedresources.MonitoredSystemGroupingExplanation,
) MonitoredSystemLedgerExplanation {
reasons := make([]MonitoredSystemLedgerExplanationReason, 0, len(explanation.Reasons))
for _, reason := range explanation.Reasons {
reasons = append(reasons, MonitoredSystemLedgerExplanationReason{
Kind: reason.Kind,
Signal: reason.Signal,
Summary: reason.Summary,
})
}
surfaces := make([]MonitoredSystemLedgerExplanationSurface, 0, len(explanation.Surfaces))
for _, surface := range explanation.Surfaces {
surfaces = append(surfaces, MonitoredSystemLedgerExplanationSurface{
Name: surface.Name,
Type: surface.Type,
Source: surface.Source,
})
}
return MonitoredSystemLedgerExplanation{
Summary: explanation.Summary,
Reasons: reasons,
Surfaces: surfaces,
}
}