mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-21 10:43:36 +00:00
df72955144
The live-setter fix for #1619 pushed backup and PMG cadences into running monitors but left pbsPollingInterval out entirely: it was neither applied to the handler's base config nor pushed to live monitors, so a saved PBS interval still waited for an unrelated restart. Add the matching override, setter and scheduler wiring, apply the value to the base config, and fan it out from the settings handler like the other cadence settings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1032 lines
27 KiB
Go
1032 lines
27 KiB
Go
package monitoring
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"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
|
|
}
|
|
|
|
// FixedIntervalPollProvider is an optional PollProvider extension for providers
|
|
// whose instances carry a user-configured poll cadence that must be honored
|
|
// exactly, bypassing adaptive interval selection.
|
|
type FixedIntervalPollProvider interface {
|
|
FixedInstanceInterval(m *Monitor, instanceName string) time.Duration
|
|
}
|
|
|
|
// 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(*Monitor) 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), 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(m *Monitor) time.Duration { return m.pbsPollingIntervalSetting() },
|
|
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(m *Monitor) time.Duration { return m.pmgPollingIntervalSetting() },
|
|
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
|
|
}
|