mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-08-22 23:43:30 +00:00
Clears the six dupl pairs in internal/monitoring: - poll_providers.go: PVE/PBS/PMG listInstances / describeInstances / connectionStatus closures collapse into generic sortedClientNames, describeProviderInstances, and providerConnectionStatuses helpers; the PBS/PMG adapters are now built by newPrefixedPollProvider from a prefixedPollProviderSpec (prefix drives status keys, health keys, and the fallback instance name). - truenas_poller.go / vmware_poller.go: the Start scheduling loop (double-start guard, stopped-channel handshake, sync+poll cadence) moves to startPollerLoop, and the active-connection config policy (enabled instances keyed by trimmed connection ID, defaults applied) moves to loadActiveInstanceConfigs, both in the new platform_poller_shared.go. - monitor_polling_vm.go / monitor_polling_containers.go: traditional polling now records guest series via the existing canonical recordGuestMetric helper (io/network sentinels preserve the historical cpu/memory/disk-only behavior on this path); the source-shape guardrails in canonical_guardrails_test.go and memory_source_catalog_test.go pin the new delegation. - mock_metrics_history.go: native and TrueNAS disk seeding share one seedDiskTelemetry closure. Proof: new TestTrueNASPollerDoubleStartKeepsSingleLoop pins the shared lifecycle loop; new TestSeedMockMetricsHistory_DiskTelemetryParityAcrossNativeAndTrueNAS pins four-series parity for both disk sources. Contract Extension Points name the shared wiring layer and poller scaffold. Full internal/monitoring test suite passes.
1025 lines
27 KiB
Go
1025 lines
27 KiB
Go
package monitoring
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
"github.com/rcourtman/pulse-go-rewrite/pkg/pbs"
|
|
"github.com/rcourtman/pulse-go-rewrite/pkg/pmg"
|
|
)
|
|
|
|
// PollProvider defines how a monitoring backend participates in scheduling and
|
|
// task execution.
|
|
type PollProvider interface {
|
|
Type() InstanceType
|
|
ListInstances(m *Monitor) []string
|
|
BaseInterval(m *Monitor) time.Duration
|
|
BuildPollTask(m *Monitor, instanceName string) (PollTask, error)
|
|
}
|
|
|
|
// PollProviderInstanceInfo describes a provider instance for health/status APIs.
|
|
type PollProviderInstanceInfo struct {
|
|
Name string
|
|
DisplayName string
|
|
Connection string
|
|
Metadata map[string]string
|
|
}
|
|
|
|
// InstanceInfoPollProvider is an optional PollProvider extension for providers
|
|
// that expose instance metadata for scheduler health responses.
|
|
type InstanceInfoPollProvider interface {
|
|
DescribeInstances(m *Monitor) []PollProviderInstanceInfo
|
|
}
|
|
|
|
// ConnectionStatusPollProvider is an optional PollProvider extension that
|
|
// publishes connection status entries keyed by external node ID (for example:
|
|
// "pve-node-a", "pbs-backup-1", "xcp-cluster-1").
|
|
type ConnectionStatusPollProvider interface {
|
|
ConnectionStatuses(m *Monitor) map[string]bool
|
|
}
|
|
|
|
// ConnectionHealthKeyPollProvider is an optional PollProvider extension for
|
|
// provider-owned connection health key normalization in monitor state.
|
|
type ConnectionHealthKeyPollProvider interface {
|
|
ConnectionHealthKey(m *Monitor, instanceName string) string
|
|
}
|
|
|
|
// SupplementalRecordsPollProvider is an optional PollProvider extension for
|
|
// providers that can emit source-native unified ingest records.
|
|
type SupplementalRecordsPollProvider interface {
|
|
SupplementalSource() unifiedresources.DataSource
|
|
SupplementalRecords(m *Monitor, orgID string) []unifiedresources.IngestRecord
|
|
}
|
|
|
|
// SupplementalChangesPollProvider is an optional PollProvider extension for
|
|
// providers that can emit canonical timeline changes alongside supplemental
|
|
// source-native records.
|
|
type SupplementalChangesPollProvider interface {
|
|
SupplementalSource() unifiedresources.DataSource
|
|
SupplementalChanges(m *Monitor, orgID string) []unifiedresources.ResourceChange
|
|
}
|
|
|
|
// SnapshotOwnedSourcesPollProvider is an optional SupplementalRecordsPollProvider
|
|
// extension for providers that fully own source-native resource ingest.
|
|
// When set, legacy snapshot slices for these sources are suppressed.
|
|
type SnapshotOwnedSourcesPollProvider interface {
|
|
SnapshotOwnedSources(m *Monitor) []unifiedresources.DataSource
|
|
}
|
|
|
|
type pollProviderAdapter struct {
|
|
instanceType InstanceType
|
|
listInstances func(*Monitor) []string
|
|
describeInstances func(*Monitor) []PollProviderInstanceInfo
|
|
connectionStatus func(*Monitor) map[string]bool
|
|
connectionKey func(*Monitor, string) string
|
|
baseInterval func(*Monitor) time.Duration
|
|
buildPollTask func(*Monitor, string) (PollTask, error)
|
|
}
|
|
|
|
func (p pollProviderAdapter) Type() InstanceType {
|
|
return p.instanceType
|
|
}
|
|
|
|
func (p pollProviderAdapter) ListInstances(m *Monitor) []string {
|
|
if p.listInstances == nil {
|
|
return nil
|
|
}
|
|
return p.listInstances(m)
|
|
}
|
|
|
|
func (p pollProviderAdapter) DescribeInstances(m *Monitor) []PollProviderInstanceInfo {
|
|
if p.describeInstances == nil {
|
|
return nil
|
|
}
|
|
return p.describeInstances(m)
|
|
}
|
|
|
|
func (p pollProviderAdapter) ConnectionStatuses(m *Monitor) map[string]bool {
|
|
if p.connectionStatus == nil {
|
|
return nil
|
|
}
|
|
return p.connectionStatus(m)
|
|
}
|
|
|
|
func (p pollProviderAdapter) ConnectionHealthKey(m *Monitor, instanceName string) string {
|
|
if p.connectionKey == nil {
|
|
return ""
|
|
}
|
|
return p.connectionKey(m, instanceName)
|
|
}
|
|
|
|
func (p pollProviderAdapter) BaseInterval(m *Monitor) time.Duration {
|
|
if p.baseInterval == nil {
|
|
return 0
|
|
}
|
|
return p.baseInterval(m)
|
|
}
|
|
|
|
func (p pollProviderAdapter) BuildPollTask(m *Monitor, instanceName string) (PollTask, error) {
|
|
if p.buildPollTask == nil {
|
|
return PollTask{}, fmt.Errorf("provider %q does not support poll task construction", p.instanceType)
|
|
}
|
|
return p.buildPollTask(m, instanceName)
|
|
}
|
|
|
|
// pollProviderInstanceConfig is one configured instance entry (name + host)
|
|
// independent of the per-platform config struct type.
|
|
type pollProviderInstanceConfig struct {
|
|
Name string
|
|
Host string
|
|
}
|
|
|
|
// sortedClientNames returns the sorted key set of a provider client map read
|
|
// under the monitor lock.
|
|
func sortedClientNames[C any](m *Monitor, clientsOf func(*Monitor) map[string]C) []string {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
m.mu.RLock()
|
|
clients := clientsOf(m)
|
|
names := make([]string, 0, len(clients))
|
|
for name := range clients {
|
|
names = append(names, name)
|
|
}
|
|
m.mu.RUnlock()
|
|
sort.Strings(names)
|
|
return names
|
|
}
|
|
|
|
// describeProviderInstances merges configured instances with live client map
|
|
// entries into the sorted instance info list shared by the PVE/PBS/PMG
|
|
// providers. configInstances and clientsOf are invoked under the monitor lock.
|
|
func describeProviderInstances[C any](
|
|
m *Monitor,
|
|
fallbackName string,
|
|
configInstances func(*Monitor) []pollProviderInstanceConfig,
|
|
clientsOf func(*Monitor) map[string]C,
|
|
) []PollProviderInstanceInfo {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
|
|
byName := make(map[string]PollProviderInstanceInfo)
|
|
m.mu.RLock()
|
|
for _, inst := range configInstances(m) {
|
|
name := strings.TrimSpace(inst.Name)
|
|
if name == "" {
|
|
name = strings.TrimSpace(inst.Host)
|
|
}
|
|
if name == "" {
|
|
name = fallbackName
|
|
}
|
|
|
|
display := strings.TrimSpace(inst.Name)
|
|
if display == "" {
|
|
display = name
|
|
}
|
|
connection := strings.TrimSpace(inst.Host)
|
|
byName[name] = PollProviderInstanceInfo{
|
|
Name: name,
|
|
DisplayName: display,
|
|
Connection: connection,
|
|
}
|
|
}
|
|
for name := range clientsOf(m) {
|
|
trimmed := strings.TrimSpace(name)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
if _, exists := byName[trimmed]; exists {
|
|
continue
|
|
}
|
|
byName[trimmed] = PollProviderInstanceInfo{Name: trimmed, DisplayName: trimmed}
|
|
}
|
|
m.mu.RUnlock()
|
|
|
|
if len(byName) == 0 {
|
|
return nil
|
|
}
|
|
names := make([]string, 0, len(byName))
|
|
for name := range byName {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
infos := make([]PollProviderInstanceInfo, 0, len(names))
|
|
for _, name := range names {
|
|
infos = append(infos, byName[name])
|
|
}
|
|
return infos
|
|
}
|
|
|
|
// providerConnectionStatuses publishes per-instance connection booleans keyed
|
|
// by "<keyPrefix><name>" for configured instances, shared by the PVE/PBS/PMG
|
|
// providers. configInstances and clientsOf are invoked under the monitor lock.
|
|
func providerConnectionStatuses[C comparable](
|
|
m *Monitor,
|
|
instanceType InstanceType,
|
|
keyPrefix string,
|
|
configInstances func(*Monitor) []pollProviderInstanceConfig,
|
|
clientsOf func(*Monitor) map[string]C,
|
|
) map[string]bool {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
|
|
var zero C
|
|
statuses := make(map[string]bool)
|
|
m.mu.RLock()
|
|
clients := clientsOf(m)
|
|
for _, inst := range configInstances(m) {
|
|
name := strings.TrimSpace(inst.Name)
|
|
if name == "" {
|
|
name = strings.TrimSpace(inst.Host)
|
|
}
|
|
if name == "" {
|
|
continue
|
|
}
|
|
key := keyPrefix + name
|
|
connected := false
|
|
if client, exists := clients[name]; exists && client != zero {
|
|
if m.state != nil && m.state.ConnectionHealth != nil {
|
|
stateKey := m.connectionHealthStateKey(instanceType, name)
|
|
connected = m.state.ConnectionHealth[stateKey]
|
|
} else {
|
|
connected = true
|
|
}
|
|
}
|
|
statuses[key] = connected
|
|
}
|
|
m.mu.RUnlock()
|
|
if len(statuses) == 0 {
|
|
return nil
|
|
}
|
|
return statuses
|
|
}
|
|
|
|
func pveInstanceConfigs(m *Monitor) []pollProviderInstanceConfig {
|
|
if m.config == nil {
|
|
return nil
|
|
}
|
|
out := make([]pollProviderInstanceConfig, 0, len(m.config.PVEInstances))
|
|
for _, inst := range m.config.PVEInstances {
|
|
out = append(out, pollProviderInstanceConfig{Name: inst.Name, Host: inst.Host})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func pbsInstanceConfigs(m *Monitor) []pollProviderInstanceConfig {
|
|
if m.config == nil {
|
|
return nil
|
|
}
|
|
out := make([]pollProviderInstanceConfig, 0, len(m.config.PBSInstances))
|
|
for _, inst := range m.config.PBSInstances {
|
|
out = append(out, pollProviderInstanceConfig{Name: inst.Name, Host: inst.Host})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func pmgInstanceConfigs(m *Monitor) []pollProviderInstanceConfig {
|
|
if m.config == nil {
|
|
return nil
|
|
}
|
|
out := make([]pollProviderInstanceConfig, 0, len(m.config.PMGInstances))
|
|
for _, inst := range m.config.PMGInstances {
|
|
out = append(out, pollProviderInstanceConfig{Name: inst.Name, Host: inst.Host})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func pveClientMap(m *Monitor) map[string]PVEClientInterface { return m.pveClients }
|
|
func pbsClientMap(m *Monitor) map[string]*pbs.Client { return m.pbsClients }
|
|
func pmgClientMap(m *Monitor) map[string]*pmg.Client { return m.pmgClients }
|
|
|
|
func newPVEPollProvider() PollProvider {
|
|
return pollProviderAdapter{
|
|
instanceType: InstanceTypePVE,
|
|
listInstances: func(m *Monitor) []string {
|
|
return sortedClientNames(m, pveClientMap)
|
|
},
|
|
describeInstances: func(m *Monitor) []PollProviderInstanceInfo {
|
|
return describeProviderInstances(m, "pve-instance", pveInstanceConfigs, pveClientMap)
|
|
},
|
|
connectionStatus: func(m *Monitor) map[string]bool {
|
|
return providerConnectionStatuses(m, InstanceTypePVE, "pve-", pveInstanceConfigs, pveClientMap)
|
|
},
|
|
connectionKey: func(_ *Monitor, instanceName string) string {
|
|
return strings.TrimSpace(instanceName)
|
|
},
|
|
baseInterval: func(m *Monitor) time.Duration {
|
|
if m == nil {
|
|
return 0
|
|
}
|
|
return m.effectivePVEPollingInterval()
|
|
},
|
|
buildPollTask: func(m *Monitor, instanceName string) (PollTask, error) {
|
|
if m == nil {
|
|
return PollTask{}, fmt.Errorf("monitor is nil")
|
|
}
|
|
client, ok := m.getPVEClient(instanceName)
|
|
if !ok || client == nil {
|
|
return PollTask{}, fmt.Errorf("PVE client missing for scheduled task")
|
|
}
|
|
return PollTask{
|
|
InstanceName: instanceName,
|
|
InstanceType: string(InstanceTypePVE),
|
|
PVEClient: client,
|
|
}, nil
|
|
},
|
|
}
|
|
}
|
|
|
|
// prefixedPollProviderSpec captures the bits that differ between the PBS and
|
|
// PMG providers, which otherwise share the full adapter wiring. prefix
|
|
// ("pbs-", "pmg-") namespaces connection status keys and connection health
|
|
// keys, and derives the "<prefix>instance" fallback display name.
|
|
type prefixedPollProviderSpec[C comparable] struct {
|
|
instanceType InstanceType
|
|
prefix string
|
|
configInstances func(*Monitor) []pollProviderInstanceConfig
|
|
clients func(*Monitor) map[string]C
|
|
pollingInterval func(*config.Config) time.Duration
|
|
buildPollTask func(*Monitor, string) (PollTask, error)
|
|
}
|
|
|
|
func newPrefixedPollProvider[C comparable](spec prefixedPollProviderSpec[C]) PollProvider {
|
|
return pollProviderAdapter{
|
|
instanceType: spec.instanceType,
|
|
listInstances: func(m *Monitor) []string {
|
|
return sortedClientNames(m, spec.clients)
|
|
},
|
|
describeInstances: func(m *Monitor) []PollProviderInstanceInfo {
|
|
return describeProviderInstances(m, spec.prefix+"instance", spec.configInstances, spec.clients)
|
|
},
|
|
connectionStatus: func(m *Monitor) map[string]bool {
|
|
return providerConnectionStatuses(m, spec.instanceType, spec.prefix, spec.configInstances, spec.clients)
|
|
},
|
|
connectionKey: func(_ *Monitor, instanceName string) string {
|
|
trimmed := strings.TrimSpace(instanceName)
|
|
if trimmed == "" {
|
|
return ""
|
|
}
|
|
return spec.prefix + trimmed
|
|
},
|
|
baseInterval: func(m *Monitor) time.Duration {
|
|
if m == nil || m.config == nil {
|
|
return 0
|
|
}
|
|
return clampInterval(spec.pollingInterval(m.config), 10*time.Second, time.Hour)
|
|
},
|
|
buildPollTask: spec.buildPollTask,
|
|
}
|
|
}
|
|
|
|
func newPBSPollProvider() PollProvider {
|
|
return newPrefixedPollProvider(prefixedPollProviderSpec[*pbs.Client]{
|
|
instanceType: InstanceTypePBS,
|
|
prefix: "pbs-",
|
|
configInstances: pbsInstanceConfigs,
|
|
clients: pbsClientMap,
|
|
pollingInterval: func(cfg *config.Config) time.Duration { return cfg.PBSPollingInterval },
|
|
buildPollTask: func(m *Monitor, instanceName string) (PollTask, error) {
|
|
if m == nil {
|
|
return PollTask{}, fmt.Errorf("monitor is nil")
|
|
}
|
|
client, ok := m.getPBSClient(instanceName)
|
|
if !ok || client == nil {
|
|
return PollTask{}, fmt.Errorf("PBS client missing for scheduled task")
|
|
}
|
|
return PollTask{
|
|
InstanceName: instanceName,
|
|
InstanceType: string(InstanceTypePBS),
|
|
PBSClient: client,
|
|
}, nil
|
|
},
|
|
})
|
|
}
|
|
|
|
func newPMGPollProvider() PollProvider {
|
|
return newPrefixedPollProvider(prefixedPollProviderSpec[*pmg.Client]{
|
|
instanceType: InstanceTypePMG,
|
|
prefix: "pmg-",
|
|
configInstances: pmgInstanceConfigs,
|
|
clients: pmgClientMap,
|
|
pollingInterval: func(cfg *config.Config) time.Duration { return cfg.PMGPollingInterval },
|
|
buildPollTask: func(m *Monitor, instanceName string) (PollTask, error) {
|
|
if m == nil {
|
|
return PollTask{}, fmt.Errorf("monitor is nil")
|
|
}
|
|
client, ok := m.getPMGClient(instanceName)
|
|
if !ok || client == nil {
|
|
return PollTask{}, fmt.Errorf("PMG client missing for scheduled task")
|
|
}
|
|
return PollTask{
|
|
InstanceName: instanceName,
|
|
InstanceType: string(InstanceTypePMG),
|
|
PMGClient: client,
|
|
}, nil
|
|
},
|
|
})
|
|
}
|
|
|
|
// RegisterPollProvider registers or replaces a polling provider for an
|
|
// instance type.
|
|
func (m *Monitor) RegisterPollProvider(provider PollProvider) error {
|
|
if m == nil {
|
|
return fmt.Errorf("monitor is nil")
|
|
}
|
|
if provider == nil {
|
|
return fmt.Errorf("poll provider is nil")
|
|
}
|
|
|
|
providerType := InstanceType(strings.TrimSpace(string(provider.Type())))
|
|
if providerType == "" {
|
|
return fmt.Errorf("poll provider type is required")
|
|
}
|
|
|
|
m.mu.Lock()
|
|
if m.pollProviders == nil {
|
|
m.pollProviders = make(map[InstanceType]PollProvider)
|
|
}
|
|
m.pollProviders[providerType] = provider
|
|
m.mu.Unlock()
|
|
|
|
m.refreshInstanceInfoCacheFromProviders()
|
|
return nil
|
|
}
|
|
|
|
func (m *Monitor) registerBuiltInPollProviders() {
|
|
_ = m.RegisterPollProvider(newPVEPollProvider())
|
|
_ = m.RegisterPollProvider(newPBSPollProvider())
|
|
_ = m.RegisterPollProvider(newPMGPollProvider())
|
|
_ = m.RegisterPollProvider(newAvailabilityPollProvider())
|
|
}
|
|
|
|
func (m *Monitor) pollProviderSnapshot() []PollProvider {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
|
|
m.mu.RLock()
|
|
if len(m.pollProviders) == 0 {
|
|
m.mu.RUnlock()
|
|
return nil
|
|
}
|
|
byType := make(map[InstanceType]PollProvider, len(m.pollProviders))
|
|
for providerType, provider := range m.pollProviders {
|
|
if provider == nil {
|
|
continue
|
|
}
|
|
if providerType == "" {
|
|
providerType = provider.Type()
|
|
}
|
|
if providerType == "" {
|
|
continue
|
|
}
|
|
byType[providerType] = provider
|
|
}
|
|
m.mu.RUnlock()
|
|
|
|
if len(byType) == 0 {
|
|
return nil
|
|
}
|
|
|
|
types := make([]string, 0, len(byType))
|
|
for providerType := range byType {
|
|
types = append(types, string(providerType))
|
|
}
|
|
sort.Strings(types)
|
|
|
|
providers := make([]PollProvider, 0, len(types))
|
|
for _, providerType := range types {
|
|
providers = append(providers, byType[InstanceType(providerType)])
|
|
}
|
|
return providers
|
|
}
|
|
|
|
func (m *Monitor) pollProviderSnapshotWithBuiltins() []PollProvider {
|
|
byType := make(map[InstanceType]PollProvider)
|
|
|
|
for _, provider := range m.pollProviderSnapshot() {
|
|
if provider == nil {
|
|
continue
|
|
}
|
|
providerType := provider.Type()
|
|
if providerType == "" {
|
|
continue
|
|
}
|
|
byType[providerType] = provider
|
|
}
|
|
|
|
builtins := []PollProvider{
|
|
newPVEPollProvider(),
|
|
newPBSPollProvider(),
|
|
newPMGPollProvider(),
|
|
newAvailabilityPollProvider(),
|
|
}
|
|
for _, provider := range builtins {
|
|
if provider == nil {
|
|
continue
|
|
}
|
|
providerType := provider.Type()
|
|
if providerType == "" {
|
|
continue
|
|
}
|
|
if _, exists := byType[providerType]; !exists {
|
|
byType[providerType] = provider
|
|
}
|
|
}
|
|
|
|
if len(byType) == 0 {
|
|
return nil
|
|
}
|
|
|
|
types := make([]string, 0, len(byType))
|
|
for providerType := range byType {
|
|
types = append(types, string(providerType))
|
|
}
|
|
sort.Strings(types)
|
|
|
|
providers := make([]PollProvider, 0, len(types))
|
|
for _, providerType := range types {
|
|
providers = append(providers, byType[InstanceType(providerType)])
|
|
}
|
|
|
|
return providers
|
|
}
|
|
|
|
func (m *Monitor) getPollProvider(instanceType InstanceType) PollProvider {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
|
|
m.mu.RLock()
|
|
provider := m.pollProviders[instanceType]
|
|
m.mu.RUnlock()
|
|
if provider != nil {
|
|
return provider
|
|
}
|
|
|
|
switch instanceType {
|
|
case InstanceTypePVE:
|
|
return newPVEPollProvider()
|
|
case InstanceTypePBS:
|
|
return newPBSPollProvider()
|
|
case InstanceTypePMG:
|
|
return newPMGPollProvider()
|
|
case InstanceTypeAvailability:
|
|
return newAvailabilityPollProvider()
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (m *Monitor) activeSchedulerKeys() map[string]struct{} {
|
|
activeKeys := make(map[string]struct{})
|
|
for _, provider := range m.pollProviderSnapshotWithBuiltins() {
|
|
if provider == nil {
|
|
continue
|
|
}
|
|
instanceType := provider.Type()
|
|
for _, instanceName := range provider.ListInstances(m) {
|
|
name := strings.TrimSpace(instanceName)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
activeKeys[schedulerKey(instanceType, name)] = struct{}{}
|
|
}
|
|
}
|
|
return activeKeys
|
|
}
|
|
|
|
func cloneProviderMetadata(in map[string]string) map[string]string {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[string]string, len(in))
|
|
for k, v := range in {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (m *Monitor) providerInstanceInfos() map[string]*instanceInfo {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
|
|
infos := make(map[string]*instanceInfo)
|
|
for _, provider := range m.pollProviderSnapshotWithBuiltins() {
|
|
if provider == nil {
|
|
continue
|
|
}
|
|
|
|
providerType := provider.Type()
|
|
if strings.TrimSpace(string(providerType)) == "" {
|
|
continue
|
|
}
|
|
|
|
addInfo := func(name, display, connection string, metadata map[string]string) {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return
|
|
}
|
|
key := schedulerKey(providerType, name)
|
|
display = strings.TrimSpace(display)
|
|
connection = strings.TrimSpace(connection)
|
|
if display == "" {
|
|
display = name
|
|
}
|
|
|
|
existing, ok := infos[key]
|
|
if !ok || existing == nil {
|
|
infos[key] = &instanceInfo{
|
|
Key: key,
|
|
Type: providerType,
|
|
DisplayName: display,
|
|
Connection: connection,
|
|
Metadata: cloneProviderMetadata(metadata),
|
|
}
|
|
return
|
|
}
|
|
|
|
if existing.Type == "" {
|
|
existing.Type = providerType
|
|
}
|
|
if existing.DisplayName == "" && display != "" {
|
|
existing.DisplayName = display
|
|
}
|
|
if existing.Connection == "" && connection != "" {
|
|
existing.Connection = connection
|
|
}
|
|
if len(metadata) == 0 {
|
|
return
|
|
}
|
|
if existing.Metadata == nil {
|
|
existing.Metadata = make(map[string]string, len(metadata))
|
|
}
|
|
for mk, mv := range metadata {
|
|
existing.Metadata[mk] = mv
|
|
}
|
|
}
|
|
|
|
if describer, ok := provider.(InstanceInfoPollProvider); ok {
|
|
for _, info := range describer.DescribeInstances(m) {
|
|
addInfo(info.Name, info.DisplayName, info.Connection, info.Metadata)
|
|
}
|
|
}
|
|
|
|
for _, name := range provider.ListInstances(m) {
|
|
addInfo(name, name, "", nil)
|
|
}
|
|
}
|
|
|
|
if len(infos) == 0 {
|
|
return nil
|
|
}
|
|
return infos
|
|
}
|
|
|
|
func (m *Monitor) refreshInstanceInfoCacheFromProviders() {
|
|
if m == nil {
|
|
return
|
|
}
|
|
|
|
infos := m.providerInstanceInfos()
|
|
|
|
m.mu.Lock()
|
|
if infos == nil {
|
|
m.instanceInfoCache = make(map[string]*instanceInfo)
|
|
} else {
|
|
m.instanceInfoCache = infos
|
|
}
|
|
m.mu.Unlock()
|
|
}
|
|
|
|
func (m *Monitor) connectionHealthStateKey(instanceType InstanceType, instanceName string) string {
|
|
trimmedName := strings.TrimSpace(instanceName)
|
|
if trimmedName == "" {
|
|
return ""
|
|
}
|
|
|
|
if provider := m.getPollProvider(instanceType); provider != nil {
|
|
if keyProvider, ok := provider.(ConnectionHealthKeyPollProvider); ok {
|
|
if key := strings.TrimSpace(keyProvider.ConnectionHealthKey(m, trimmedName)); key != "" {
|
|
return key
|
|
}
|
|
}
|
|
}
|
|
|
|
trimmedType := strings.TrimSpace(string(instanceType))
|
|
if trimmedType == "" {
|
|
return trimmedName
|
|
}
|
|
return trimmedType + "-" + trimmedName
|
|
}
|
|
|
|
func (m *Monitor) setProviderConnectionHealth(instanceType InstanceType, instanceName string, healthy bool) {
|
|
if m == nil || m.state == nil {
|
|
return
|
|
}
|
|
key := m.connectionHealthStateKey(instanceType, instanceName)
|
|
if key == "" {
|
|
return
|
|
}
|
|
m.state.SetConnectionHealth(key, healthy)
|
|
}
|
|
|
|
func (m *Monitor) removeProviderConnectionHealth(instanceType InstanceType, instanceName string) {
|
|
if m == nil || m.state == nil {
|
|
return
|
|
}
|
|
key := m.connectionHealthStateKey(instanceType, instanceName)
|
|
if key == "" {
|
|
return
|
|
}
|
|
m.state.RemoveConnectionHealth(key)
|
|
}
|
|
|
|
func (m *Monitor) providerConnectionStatuses(provider PollProvider) map[string]bool {
|
|
if m == nil || provider == nil {
|
|
return nil
|
|
}
|
|
|
|
if statusProvider, ok := provider.(ConnectionStatusPollProvider); ok {
|
|
statuses := statusProvider.ConnectionStatuses(m)
|
|
out := make(map[string]bool, len(statuses))
|
|
for key, connected := range statuses {
|
|
trimmed := strings.TrimSpace(key)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
out[trimmed] = connected
|
|
}
|
|
if len(out) > 0 {
|
|
return out
|
|
}
|
|
}
|
|
|
|
instanceType := strings.TrimSpace(string(provider.Type()))
|
|
if instanceType == "" {
|
|
return nil
|
|
}
|
|
|
|
names := provider.ListInstances(m)
|
|
if len(names) == 0 {
|
|
return nil
|
|
}
|
|
|
|
connectionHealth := make(map[string]bool)
|
|
m.mu.RLock()
|
|
if m.state != nil && m.state.ConnectionHealth != nil {
|
|
connectionHealth = make(map[string]bool, len(m.state.ConnectionHealth))
|
|
for key, healthy := range m.state.ConnectionHealth {
|
|
connectionHealth[key] = healthy
|
|
}
|
|
}
|
|
m.mu.RUnlock()
|
|
|
|
statuses := make(map[string]bool, len(names))
|
|
for _, name := range names {
|
|
trimmedName := strings.TrimSpace(name)
|
|
if trimmedName == "" {
|
|
continue
|
|
}
|
|
|
|
key := instanceType + "-" + trimmedName
|
|
stateKey := m.connectionHealthStateKey(InstanceType(instanceType), trimmedName)
|
|
if stateKey == "" {
|
|
stateKey = trimmedName
|
|
}
|
|
connected := false
|
|
if healthy, exists := connectionHealth[stateKey]; exists {
|
|
connected = healthy
|
|
} else if healthy, exists := connectionHealth[key]; exists {
|
|
connected = healthy
|
|
} else if healthy, exists := connectionHealth[trimmedName]; exists {
|
|
connected = healthy
|
|
}
|
|
statuses[key] = connected
|
|
}
|
|
|
|
if len(statuses) == 0 {
|
|
return nil
|
|
}
|
|
return statuses
|
|
}
|
|
|
|
func (m *Monitor) providerOwnedSnapshotSources() []unifiedresources.DataSource {
|
|
owned := make(map[string]unifiedresources.DataSource)
|
|
|
|
providers := m.pollProviderSnapshotWithBuiltins()
|
|
for _, provider := range providers {
|
|
if provider == nil {
|
|
continue
|
|
}
|
|
|
|
// Snapshot source ownership is only meaningful for providers that can
|
|
// emit source-native records.
|
|
if _, ok := provider.(SupplementalRecordsPollProvider); !ok {
|
|
continue
|
|
}
|
|
owner, ok := provider.(SnapshotOwnedSourcesPollProvider)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
for _, source := range owner.SnapshotOwnedSources(m) {
|
|
key := strings.ToLower(strings.TrimSpace(string(source)))
|
|
if key == "" {
|
|
continue
|
|
}
|
|
owned[key] = unifiedresources.DataSource(key)
|
|
}
|
|
}
|
|
|
|
orgID := "default"
|
|
if m != nil {
|
|
if trimmed := strings.TrimSpace(m.orgID); trimmed != "" {
|
|
orgID = trimmed
|
|
}
|
|
}
|
|
for _, provider := range m.supplementalProviderSnapshot() {
|
|
if provider == nil {
|
|
continue
|
|
}
|
|
|
|
var sources []unifiedresources.DataSource
|
|
if tenantOwner, ok := provider.(interface {
|
|
SnapshotOwnedSourcesForOrg(string) []unifiedresources.DataSource
|
|
}); ok {
|
|
sources = tenantOwner.SnapshotOwnedSourcesForOrg(orgID)
|
|
} else if owner, ok := provider.(interface {
|
|
SnapshotOwnedSources() []unifiedresources.DataSource
|
|
}); ok {
|
|
sources = owner.SnapshotOwnedSources()
|
|
}
|
|
|
|
for _, source := range sources {
|
|
key := strings.ToLower(strings.TrimSpace(string(source)))
|
|
if key == "" {
|
|
continue
|
|
}
|
|
owned[key] = unifiedresources.DataSource(key)
|
|
}
|
|
}
|
|
|
|
if len(owned) == 0 {
|
|
return nil
|
|
}
|
|
|
|
keys := make([]string, 0, len(owned))
|
|
for key := range owned {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
sources := make([]unifiedresources.DataSource, 0, len(keys))
|
|
for _, key := range keys {
|
|
sources = append(sources, owned[key])
|
|
}
|
|
return sources
|
|
}
|
|
|
|
func (m *Monitor) supplementalProviderSnapshot() map[unifiedresources.DataSource]MonitorSupplementalRecordsProvider {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
|
|
m.mu.RLock()
|
|
if len(m.supplementalProviders) == 0 {
|
|
m.mu.RUnlock()
|
|
return nil
|
|
}
|
|
snapshot := make(map[unifiedresources.DataSource]MonitorSupplementalRecordsProvider, len(m.supplementalProviders))
|
|
for source, provider := range m.supplementalProviders {
|
|
if provider == nil {
|
|
continue
|
|
}
|
|
normalized := unifiedresources.DataSource(strings.ToLower(strings.TrimSpace(string(source))))
|
|
if normalized == "" {
|
|
continue
|
|
}
|
|
snapshot[normalized] = provider
|
|
}
|
|
m.mu.RUnlock()
|
|
|
|
if len(snapshot) == 0 {
|
|
return nil
|
|
}
|
|
return snapshot
|
|
}
|
|
|
|
func (m *Monitor) collectSupplementalRecordsBySource() map[unifiedresources.DataSource][]unifiedresources.IngestRecord {
|
|
providers := m.pollProviderSnapshotWithBuiltins()
|
|
manualProviders := m.supplementalProviderSnapshot()
|
|
if len(providers) == 0 && len(manualProviders) == 0 {
|
|
return nil
|
|
}
|
|
|
|
orgID := "default"
|
|
if m != nil {
|
|
if trimmed := strings.TrimSpace(m.orgID); trimmed != "" {
|
|
orgID = trimmed
|
|
}
|
|
}
|
|
|
|
recordsBySource := make(map[unifiedresources.DataSource][]unifiedresources.IngestRecord)
|
|
for _, provider := range providers {
|
|
supplementalProvider, ok := provider.(SupplementalRecordsPollProvider)
|
|
if !ok {
|
|
continue
|
|
}
|
|
source := supplementalProvider.SupplementalSource()
|
|
if strings.TrimSpace(string(source)) == "" {
|
|
continue
|
|
}
|
|
records := supplementalProvider.SupplementalRecords(m, orgID)
|
|
if len(records) == 0 {
|
|
continue
|
|
}
|
|
recordsBySource[source] = append(recordsBySource[source], records...)
|
|
}
|
|
|
|
if len(recordsBySource) == 0 {
|
|
recordsBySource = nil
|
|
}
|
|
|
|
if len(manualProviders) > 0 {
|
|
sources := make([]string, 0, len(manualProviders))
|
|
for source := range manualProviders {
|
|
sources = append(sources, string(source))
|
|
}
|
|
sort.Strings(sources)
|
|
|
|
if recordsBySource == nil {
|
|
recordsBySource = make(map[unifiedresources.DataSource][]unifiedresources.IngestRecord, len(sources))
|
|
}
|
|
for _, sourceName := range sources {
|
|
source := unifiedresources.DataSource(sourceName)
|
|
provider := manualProviders[source]
|
|
if provider == nil {
|
|
continue
|
|
}
|
|
records := provider.SupplementalRecords(m, orgID)
|
|
if len(records) == 0 {
|
|
continue
|
|
}
|
|
recordsBySource[source] = append(recordsBySource[source], records...)
|
|
}
|
|
}
|
|
|
|
if len(recordsBySource) == 0 {
|
|
return nil
|
|
}
|
|
return recordsBySource
|
|
}
|
|
|
|
func (m *Monitor) collectSupplementalChanges() []unifiedresources.ResourceChange {
|
|
providers := m.pollProviderSnapshotWithBuiltins()
|
|
manualProviders := m.supplementalProviderSnapshot()
|
|
if len(providers) == 0 && len(manualProviders) == 0 {
|
|
return nil
|
|
}
|
|
|
|
orgID := "default"
|
|
if m != nil {
|
|
if trimmed := strings.TrimSpace(m.orgID); trimmed != "" {
|
|
orgID = trimmed
|
|
}
|
|
}
|
|
|
|
changes := make([]unifiedresources.ResourceChange, 0)
|
|
for _, provider := range providers {
|
|
changeProvider, ok := provider.(SupplementalChangesPollProvider)
|
|
if !ok {
|
|
continue
|
|
}
|
|
changes = append(changes, changeProvider.SupplementalChanges(m, orgID)...)
|
|
}
|
|
|
|
if len(manualProviders) > 0 {
|
|
sources := make([]string, 0, len(manualProviders))
|
|
for source := range manualProviders {
|
|
sources = append(sources, string(source))
|
|
}
|
|
sort.Strings(sources)
|
|
for _, sourceName := range sources {
|
|
source := unifiedresources.DataSource(sourceName)
|
|
provider := manualProviders[source]
|
|
changeProvider, ok := provider.(MonitorSupplementalChangesProvider)
|
|
if !ok || changeProvider == nil {
|
|
continue
|
|
}
|
|
changes = append(changes, changeProvider.SupplementalChanges(m, orgID)...)
|
|
}
|
|
}
|
|
|
|
if len(changes) == 0 {
|
|
return nil
|
|
}
|
|
return changes
|
|
}
|