mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-13 15:28:38 +00:00
478 lines
13 KiB
Go
478 lines
13 KiB
Go
package unifiedresources
|
|
|
|
import (
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
)
|
|
|
|
// MonitorAdapter exposes a ResourceRegistry through the monitoring
|
|
// package's legacy resource-store contract.
|
|
type MonitorAdapter struct {
|
|
registry *ResourceRegistry
|
|
|
|
mu sync.RWMutex
|
|
activeAlerts []models.Alert
|
|
lastRebuiltAt time.Time
|
|
}
|
|
|
|
// NewMonitorAdapter creates a monitor-facing adapter around a registry.
|
|
// If registry is nil, a new in-memory registry is created.
|
|
func NewMonitorAdapter(registry *ResourceRegistry) *MonitorAdapter {
|
|
if registry == nil {
|
|
registry = NewRegistry(nil)
|
|
}
|
|
|
|
return &MonitorAdapter{
|
|
registry: registry,
|
|
}
|
|
}
|
|
|
|
func (a *MonitorAdapter) currentRegistry() *ResourceRegistry {
|
|
if a == nil {
|
|
return nil
|
|
}
|
|
|
|
a.mu.RLock()
|
|
registry := a.registry
|
|
a.mu.RUnlock()
|
|
return registry
|
|
}
|
|
|
|
// RecordChange forwards canonical resource-history events into the underlying
|
|
// resource store when monitoring has a durable store attached.
|
|
func (a *MonitorAdapter) RecordChange(change ResourceChange) error {
|
|
registry := a.currentRegistry()
|
|
if registry == nil || registry.store == nil {
|
|
return nil
|
|
}
|
|
return registry.store.RecordChange(change)
|
|
}
|
|
|
|
// GetRecentChanges forwards canonical resource-history reads into the
|
|
// underlying resource store when monitoring has a durable store attached.
|
|
func (a *MonitorAdapter) GetRecentChanges(canonicalID string, since time.Time, limit int) ([]ResourceChange, error) {
|
|
registry := a.currentRegistry()
|
|
if registry == nil || registry.store == nil {
|
|
return nil, nil
|
|
}
|
|
return registry.store.GetRecentChanges(canonicalID, since, limit)
|
|
}
|
|
|
|
func (a *MonitorAdapter) replaceRegistry(snapshot models.StateSnapshot, recordsBySource map[DataSource][]IngestRecord) {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return
|
|
}
|
|
|
|
before := registry.List()
|
|
rebuilt := NewRegistry(registry.store)
|
|
rebuilt.IngestSnapshot(snapshot)
|
|
for source, records := range recordsBySource {
|
|
if len(records) == 0 || strings.TrimSpace(string(source)) == "" {
|
|
continue
|
|
}
|
|
rebuilt.IngestRecords(source, records)
|
|
}
|
|
rebuiltAt := time.Now().UTC()
|
|
if !snapshot.LastUpdate.IsZero() && snapshot.LastUpdate.After(rebuiltAt) {
|
|
rebuiltAt = snapshot.LastUpdate
|
|
}
|
|
|
|
var occurredAt *time.Time
|
|
if !snapshot.LastUpdate.IsZero() {
|
|
occ := snapshot.LastUpdate
|
|
occurredAt = &occ
|
|
}
|
|
recordRegistryChanges(registry.store, before, rebuilt.List(), rebuiltAt, occurredAt, SourcePulseDiff, "")
|
|
|
|
a.mu.Lock()
|
|
a.registry = rebuilt
|
|
a.activeAlerts = append([]models.Alert(nil), snapshot.ActiveAlerts...)
|
|
a.lastRebuiltAt = rebuiltAt
|
|
a.mu.Unlock()
|
|
}
|
|
|
|
// ShouldSkipAPIPolling returns true when agent coverage indicates API
|
|
// polling for the hostname should be skipped entirely.
|
|
func (a *MonitorAdapter) ShouldSkipAPIPolling(hostname string) bool {
|
|
hostname = strings.ToLower(strings.TrimSpace(hostname))
|
|
if hostname == "" {
|
|
return false
|
|
}
|
|
|
|
multiplier, ok := a.GetPollingRecommendations()[hostname]
|
|
if !ok {
|
|
return false
|
|
}
|
|
return multiplier == 0
|
|
}
|
|
|
|
// GetPollingRecommendations returns hostname -> polling multiplier.
|
|
// 0 means skip API polling; 0.5 means reduced frequency.
|
|
func (a *MonitorAdapter) GetPollingRecommendations() map[string]float64 {
|
|
recommendations := make(map[string]float64)
|
|
|
|
for _, resource := range a.GetAll() {
|
|
sourceType := monitorSourceType(resource.Sources)
|
|
if sourceType != "agent" && sourceType != "hybrid" {
|
|
continue
|
|
}
|
|
|
|
hostname := monitorHostname(resource)
|
|
if hostname == "" {
|
|
hostname = strings.TrimSpace(resource.Name)
|
|
}
|
|
if hostname == "" {
|
|
continue
|
|
}
|
|
|
|
key := strings.ToLower(hostname)
|
|
if sourceType == "hybrid" {
|
|
if existing, exists := recommendations[key]; !exists || existing != 0 {
|
|
recommendations[key] = 0.5
|
|
}
|
|
continue
|
|
}
|
|
recommendations[key] = 0
|
|
}
|
|
|
|
return recommendations
|
|
}
|
|
|
|
// GetAll returns all unified resources for monitor broadcast usage.
|
|
func (a *MonitorAdapter) GetAll() []Resource {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
|
|
return registry.List()
|
|
}
|
|
|
|
func (a *MonitorAdapter) unifiedAIAdapter() *UnifiedAIAdapter {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return &UnifiedAIAdapter{registry: registry}
|
|
}
|
|
|
|
// GetInfrastructure returns infrastructure resources for unified AI context.
|
|
func (a *MonitorAdapter) GetInfrastructure() []Resource {
|
|
adapter := a.unifiedAIAdapter()
|
|
if adapter == nil {
|
|
return nil
|
|
}
|
|
return adapter.GetInfrastructure()
|
|
}
|
|
|
|
// GetWorkloads returns workload resources for unified AI context.
|
|
func (a *MonitorAdapter) GetWorkloads() []Resource {
|
|
adapter := a.unifiedAIAdapter()
|
|
if adapter == nil {
|
|
return nil
|
|
}
|
|
return adapter.GetWorkloads()
|
|
}
|
|
|
|
// GetByType returns resources filtered by type for MCP tools.
|
|
func (a *MonitorAdapter) GetByType(t ResourceType) []Resource {
|
|
adapter := a.unifiedAIAdapter()
|
|
if adapter == nil {
|
|
return nil
|
|
}
|
|
return adapter.GetByType(t)
|
|
}
|
|
|
|
// GetStats returns aggregate stats for unified AI context.
|
|
func (a *MonitorAdapter) GetStats() ResourceStats {
|
|
adapter := a.unifiedAIAdapter()
|
|
if adapter == nil {
|
|
return ResourceStats{
|
|
ByType: make(map[ResourceType]int),
|
|
ByStatus: make(map[ResourceStatus]int),
|
|
BySource: make(map[DataSource]int),
|
|
}
|
|
}
|
|
return adapter.GetStats()
|
|
}
|
|
|
|
// GetTopByCPU returns top CPU resources for unified AI context.
|
|
func (a *MonitorAdapter) GetTopByCPU(limit int, types []ResourceType) []Resource {
|
|
adapter := a.unifiedAIAdapter()
|
|
if adapter == nil {
|
|
return nil
|
|
}
|
|
return adapter.GetTopByCPU(limit, types)
|
|
}
|
|
|
|
// GetTopByMemory returns top memory resources for unified AI context.
|
|
func (a *MonitorAdapter) GetTopByMemory(limit int, types []ResourceType) []Resource {
|
|
adapter := a.unifiedAIAdapter()
|
|
if adapter == nil {
|
|
return nil
|
|
}
|
|
return adapter.GetTopByMemory(limit, types)
|
|
}
|
|
|
|
// GetTopByDisk returns top disk resources for unified AI context.
|
|
func (a *MonitorAdapter) GetTopByDisk(limit int, types []ResourceType) []Resource {
|
|
adapter := a.unifiedAIAdapter()
|
|
if adapter == nil {
|
|
return nil
|
|
}
|
|
return adapter.GetTopByDisk(limit, types)
|
|
}
|
|
|
|
// GetRelated returns related resources for unified AI context.
|
|
func (a *MonitorAdapter) GetRelated(resourceID string) map[string][]Resource {
|
|
adapter := a.unifiedAIAdapter()
|
|
if adapter == nil {
|
|
return map[string][]Resource{}
|
|
}
|
|
return adapter.GetRelated(resourceID)
|
|
}
|
|
|
|
// FindContainerHost resolves a container host for unified AI context.
|
|
func (a *MonitorAdapter) FindContainerHost(containerNameOrID string) string {
|
|
adapter := a.unifiedAIAdapter()
|
|
if adapter == nil {
|
|
return ""
|
|
}
|
|
return adapter.FindContainerHost(containerNameOrID)
|
|
}
|
|
|
|
// PopulateFromSnapshot ingests a fresh state snapshot into the registry.
|
|
func (a *MonitorAdapter) PopulateFromSnapshot(snapshot models.StateSnapshot) {
|
|
a.replaceRegistry(snapshot, nil)
|
|
}
|
|
|
|
// PopulateSnapshotAndSupplemental atomically rebuilds the registry from a
|
|
// snapshot plus source-native supplemental records before exposing the result.
|
|
func (a *MonitorAdapter) PopulateSnapshotAndSupplemental(snapshot models.StateSnapshot, recordsBySource map[DataSource][]IngestRecord) {
|
|
a.replaceRegistry(snapshot, recordsBySource)
|
|
}
|
|
|
|
// PopulateSupplementalRecords ingests source-native records emitted outside the
|
|
// legacy state snapshot pipeline.
|
|
func (a *MonitorAdapter) PopulateSupplementalRecords(source DataSource, records []IngestRecord) {
|
|
registry := a.currentRegistry()
|
|
if registry == nil || len(records) == 0 || strings.TrimSpace(string(source)) == "" {
|
|
return
|
|
}
|
|
before := registry.List()
|
|
registry.IngestRecords(source, records)
|
|
rebuiltAt := time.Now().UTC()
|
|
recordRegistryChanges(registry.store, before, registry.List(), rebuiltAt, nil, SourcePlatformEvent, changeSourceAdapterForDataSource(source))
|
|
a.mu.Lock()
|
|
a.lastRebuiltAt = rebuiltAt
|
|
a.mu.Unlock()
|
|
}
|
|
|
|
// MetricsTargetForResource resolves the history/metrics target for a canonical
|
|
// resource currently held by the adapter.
|
|
func (a *MonitorAdapter) MetricsTargetForResource(resourceID string) *MetricsTarget {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return registry.MetricsTarget(resourceID)
|
|
}
|
|
|
|
// UnifiedResourceFreshness returns the most recent point at which the adapter
|
|
// replaced or mutated its canonical registry contents.
|
|
func (a *MonitorAdapter) UnifiedResourceFreshness() time.Time {
|
|
if a == nil {
|
|
return time.Time{}
|
|
}
|
|
|
|
a.mu.RLock()
|
|
defer a.mu.RUnlock()
|
|
return a.lastRebuiltAt
|
|
}
|
|
|
|
// VMs returns cached VM views for AI/read-state consumers.
|
|
func (a *MonitorAdapter) VMs() []*VMView {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return registry.VMs()
|
|
}
|
|
|
|
// Containers returns cached LXC container views for AI/read-state consumers.
|
|
func (a *MonitorAdapter) Containers() []*ContainerView {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return registry.Containers()
|
|
}
|
|
|
|
// Nodes returns cached Proxmox node views for AI/read-state consumers.
|
|
func (a *MonitorAdapter) Nodes() []*NodeView {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return registry.Nodes()
|
|
}
|
|
|
|
// Hosts returns cached agent-host views for AI/read-state consumers.
|
|
func (a *MonitorAdapter) Hosts() []*HostView {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return registry.Hosts()
|
|
}
|
|
|
|
// DockerHosts returns cached Docker host views for AI/read-state consumers.
|
|
func (a *MonitorAdapter) DockerHosts() []*DockerHostView {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return registry.DockerHosts()
|
|
}
|
|
|
|
// DockerContainers returns cached Docker container views for AI/read-state consumers.
|
|
func (a *MonitorAdapter) DockerContainers() []*DockerContainerView {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return registry.DockerContainers()
|
|
}
|
|
|
|
// StoragePools returns cached storage pool views for AI/read-state consumers.
|
|
func (a *MonitorAdapter) StoragePools() []*StoragePoolView {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return registry.StoragePools()
|
|
}
|
|
|
|
// PhysicalDisks returns cached physical disk views for AI/read-state consumers.
|
|
func (a *MonitorAdapter) PhysicalDisks() []*PhysicalDiskView {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return registry.PhysicalDisks()
|
|
}
|
|
|
|
// PBSInstances returns cached PBS instance views for AI/read-state consumers.
|
|
func (a *MonitorAdapter) PBSInstances() []*PBSInstanceView {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return registry.PBSInstances()
|
|
}
|
|
|
|
// PMGInstances returns cached PMG instance views for AI/read-state consumers.
|
|
func (a *MonitorAdapter) PMGInstances() []*PMGInstanceView {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return registry.PMGInstances()
|
|
}
|
|
|
|
// K8sClusters returns cached Kubernetes cluster views for AI/read-state consumers.
|
|
func (a *MonitorAdapter) K8sClusters() []*K8sClusterView {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return registry.K8sClusters()
|
|
}
|
|
|
|
// K8sNodes returns cached Kubernetes node views for AI/read-state consumers.
|
|
func (a *MonitorAdapter) K8sNodes() []*K8sNodeView {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return registry.K8sNodes()
|
|
}
|
|
|
|
// Pods returns cached pod views for AI/read-state consumers.
|
|
func (a *MonitorAdapter) Pods() []*PodView {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return registry.Pods()
|
|
}
|
|
|
|
// K8sDeployments returns cached deployment views for AI/read-state consumers.
|
|
func (a *MonitorAdapter) K8sDeployments() []*K8sDeploymentView {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return registry.K8sDeployments()
|
|
}
|
|
|
|
// Workloads returns cached polymorphic workload views for AI/read-state consumers.
|
|
func (a *MonitorAdapter) Workloads() []*WorkloadView {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return registry.Workloads()
|
|
}
|
|
|
|
// Infrastructure returns cached polymorphic infrastructure views for AI/read-state consumers.
|
|
func (a *MonitorAdapter) Infrastructure() []*InfrastructureView {
|
|
registry := a.currentRegistry()
|
|
if registry == nil {
|
|
return nil
|
|
}
|
|
return registry.Infrastructure()
|
|
}
|
|
|
|
func monitorSourceType(sources []DataSource) string {
|
|
if len(sources) > 1 {
|
|
return "hybrid"
|
|
}
|
|
if len(sources) == 1 {
|
|
switch sources[0] {
|
|
case SourceAgent, SourceDocker, SourceK8s:
|
|
return "agent"
|
|
default:
|
|
return "api"
|
|
}
|
|
}
|
|
return "api"
|
|
}
|
|
|
|
func monitorHostname(resource Resource) string {
|
|
if resource.Agent != nil {
|
|
if hostname := strings.TrimSpace(resource.Agent.Hostname); hostname != "" {
|
|
return hostname
|
|
}
|
|
}
|
|
if resource.Docker != nil {
|
|
if hostname := strings.TrimSpace(resource.Docker.Hostname); hostname != "" {
|
|
return hostname
|
|
}
|
|
}
|
|
if resource.Proxmox != nil {
|
|
if hostname := strings.TrimSpace(resource.Proxmox.NodeName); hostname != "" {
|
|
return hostname
|
|
}
|
|
}
|
|
for _, hostname := range resource.Identity.Hostnames {
|
|
if trimmed := strings.TrimSpace(hostname); trimmed != "" {
|
|
return trimmed
|
|
}
|
|
}
|
|
return ""
|
|
}
|