mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Fix storage trend metrics targets
This commit is contained in:
parent
fbcb77d635
commit
a36bb254d5
4 changed files with 163 additions and 5 deletions
|
|
@ -196,7 +196,13 @@ carry the canonical `Resource.Sources` array onto `ResourceFrontend` and
|
|||
source/facet instead of treating the `storage` resource type as Proxmox by
|
||||
default. Appliance presentation details such as Unraid array identity may remain
|
||||
inside storage metadata, but agent-backed storage must stay canonical
|
||||
`platformType=agent`.
|
||||
`platformType=agent`. That same broadcast resource contract owns resolved
|
||||
metrics targets. Monitoring must enrich broadcast/state `ResourceFrontend`
|
||||
payloads from the active metrics-target read-state before serialization so
|
||||
`/api/state` and websocket consumers use the same canonical metrics history IDs
|
||||
as `/api/resources`; storage resources must not fall back to generated
|
||||
`Resource.ID` values when the unified resource registry can resolve a
|
||||
source-owned `storage` target.
|
||||
Unraid ingest must preserve the agent's native disk topology fields through the
|
||||
monitoring model and read-state projection. `internal/monitoring/monitor_agents.go`
|
||||
and `internal/monitoring/monitor.go` must carry model, transport, filesystem,
|
||||
|
|
|
|||
|
|
@ -222,10 +222,12 @@ func TestBroadcastResourceProjectionCoalescesSplitHostIdentities(t *testing.T) {
|
|||
source := string(data)
|
||||
|
||||
for _, snippet := range []string{
|
||||
"metricsTargetResolver := broadcastMetricsTargetResolver(unifiedView.readState)",
|
||||
"broadcastResources := coalesceBroadcastResources(unifiedView.resources)",
|
||||
"frontendState.Resources = convertResourcesForBroadcast(broadcastResources)",
|
||||
"frontendState.Resources = convertResourcesForBroadcast(broadcastResources, metricsTargetResolver)",
|
||||
"frontendState.ConnectedInfrastructure = buildConnectedInfrastructure(broadcastResources, snapshot)",
|
||||
"func coalesceBroadcastResources(resources []unifiedresources.Resource) []unifiedresources.Resource {",
|
||||
"func attachBroadcastMetricsTargets(",
|
||||
"func shouldMergeBroadcastHostResources(left, right unifiedresources.Resource) bool {",
|
||||
"broadcastHasSource(sources, unifiedresources.SourceAgent) && broadcastHasRuntimePlatformSource(sources)",
|
||||
} {
|
||||
|
|
|
|||
|
|
@ -3726,8 +3726,9 @@ func (m *Monitor) buildBroadcastFrontendStateFromSnapshot(snapshot models.StateS
|
|||
}
|
||||
}
|
||||
unifiedView := m.currentUnifiedStateView()
|
||||
metricsTargetResolver := broadcastMetricsTargetResolver(unifiedView.readState)
|
||||
broadcastResources := coalesceBroadcastResources(unifiedView.resources)
|
||||
frontendState.Resources = convertResourcesForBroadcast(broadcastResources)
|
||||
frontendState.Resources = convertResourcesForBroadcast(broadcastResources, metricsTargetResolver)
|
||||
frontendState.ConnectedInfrastructure = buildConnectedInfrastructure(broadcastResources, snapshot)
|
||||
if !unifiedView.freshness.IsZero() {
|
||||
frontendState.LastUpdate = unifiedView.freshness.UnixMilli()
|
||||
|
|
@ -4898,7 +4899,13 @@ func (m *Monitor) getUnifiedResourcesForBroadcast() []unifiedresources.Resource
|
|||
// getResourcesForBroadcast retrieves all resources from the store and converts
|
||||
// them to frontend format.
|
||||
func (m *Monitor) getResourcesForBroadcast() []models.ResourceFrontend {
|
||||
return convertResourcesForBroadcast(m.getUnifiedResourcesForBroadcast())
|
||||
m.mu.RLock()
|
||||
store := m.resourceStore
|
||||
m.mu.RUnlock()
|
||||
return convertResourcesForBroadcast(
|
||||
m.getUnifiedResourcesForBroadcast(),
|
||||
broadcastMetricsTargetResolver(store),
|
||||
)
|
||||
}
|
||||
|
||||
func coalesceBroadcastResources(resources []unifiedresources.Resource) []unifiedresources.Resource {
|
||||
|
|
@ -5215,10 +5222,17 @@ func betterBroadcastStatus(left, right unifiedresources.ResourceStatus) unifiedr
|
|||
}
|
||||
|
||||
// convertResourcesForBroadcast converts unified resources into the frontend payload shape.
|
||||
func convertResourcesForBroadcast(allResources []unifiedresources.Resource) []models.ResourceFrontend {
|
||||
func convertResourcesForBroadcast(
|
||||
allResources []unifiedresources.Resource,
|
||||
metricsTargetResolvers ...MetricsTargetResourceStore,
|
||||
) []models.ResourceFrontend {
|
||||
if len(allResources) == 0 {
|
||||
return []models.ResourceFrontend{}
|
||||
}
|
||||
allResources = attachBroadcastMetricsTargets(
|
||||
allResources,
|
||||
firstBroadcastMetricsTargetResolver(metricsTargetResolvers),
|
||||
)
|
||||
allResources = coalesceBroadcastResources(allResources)
|
||||
type broadcastResource struct {
|
||||
input models.ResourceConvertInput
|
||||
|
|
@ -5254,6 +5268,62 @@ func convertResourcesForBroadcast(allResources []unifiedresources.Resource) []mo
|
|||
return result
|
||||
}
|
||||
|
||||
func broadcastMetricsTargetResolver(source interface{}) MetricsTargetResourceStore {
|
||||
resolver, ok := source.(MetricsTargetResourceStore)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return resolver
|
||||
}
|
||||
|
||||
func firstBroadcastMetricsTargetResolver(
|
||||
resolvers []MetricsTargetResourceStore,
|
||||
) MetricsTargetResourceStore {
|
||||
for _, resolver := range resolvers {
|
||||
if resolver != nil {
|
||||
return resolver
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func attachBroadcastMetricsTargets(
|
||||
resources []unifiedresources.Resource,
|
||||
resolver MetricsTargetResourceStore,
|
||||
) []unifiedresources.Resource {
|
||||
if len(resources) == 0 || resolver == nil {
|
||||
return resources
|
||||
}
|
||||
|
||||
out := make([]unifiedresources.Resource, len(resources))
|
||||
for i, resource := range resources {
|
||||
if resource.MetricsTarget == nil {
|
||||
resource.MetricsTarget = validBroadcastMetricsTarget(
|
||||
resolver.MetricsTargetForResource(resource.ID),
|
||||
)
|
||||
}
|
||||
out[i] = resource
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func validBroadcastMetricsTarget(
|
||||
target *unifiedresources.MetricsTarget,
|
||||
) *unifiedresources.MetricsTarget {
|
||||
if target == nil {
|
||||
return nil
|
||||
}
|
||||
resourceType := strings.TrimSpace(target.ResourceType)
|
||||
resourceID := strings.TrimSpace(target.ResourceID)
|
||||
if resourceType == "" || resourceID == "" {
|
||||
return nil
|
||||
}
|
||||
return &unifiedresources.MetricsTarget{
|
||||
ResourceType: resourceType,
|
||||
ResourceID: resourceID,
|
||||
}
|
||||
}
|
||||
|
||||
func monitorResourceToConvertInput(resource unifiedresources.Resource) models.ResourceConvertInput {
|
||||
resourceType := monitorFrontendResourceType(resource)
|
||||
if resource.DiscoveryTarget == nil {
|
||||
|
|
|
|||
|
|
@ -152,6 +152,86 @@ func TestConvertResourcesForBroadcastCoalescesSplitHostResources(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestConvertResourcesForBroadcastAttachesResolvedStorageMetricsTarget(t *testing.T) {
|
||||
now := time.Date(2026, 5, 14, 10, 45, 0, 0, time.UTC)
|
||||
agentID := "de6d3fee-2595-6c2b-6b08-43db6b0ab427"
|
||||
storageID := "storage-2642e9fca16b2f87"
|
||||
parentID := agentID
|
||||
total := int64(24_000_000_000_000)
|
||||
used := int64(16_900_000_000_000)
|
||||
|
||||
registry := unifiedresources.NewRegistry(nil)
|
||||
registry.IngestResources([]unifiedresources.Resource{
|
||||
{
|
||||
ID: agentID,
|
||||
Type: unifiedresources.ResourceTypeAgent,
|
||||
Name: "Tower",
|
||||
Status: unifiedresources.StatusOnline,
|
||||
LastSeen: now,
|
||||
Sources: []unifiedresources.DataSource{unifiedresources.SourceAgent},
|
||||
Agent: &unifiedresources.AgentData{
|
||||
AgentID: agentID,
|
||||
Hostname: "Tower",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: storageID,
|
||||
Type: unifiedresources.ResourceTypeStorage,
|
||||
Name: "Tower Array",
|
||||
Status: unifiedresources.StatusWarning,
|
||||
LastSeen: now,
|
||||
Sources: []unifiedresources.DataSource{unifiedresources.SourceAgent},
|
||||
ParentID: &parentID,
|
||||
Metrics: &unifiedresources.ResourceMetrics{
|
||||
Disk: &unifiedresources.MetricValue{
|
||||
Used: &used,
|
||||
Total: &total,
|
||||
Percent: 70.4,
|
||||
Unit: "bytes",
|
||||
Source: unifiedresources.SourceAgent,
|
||||
},
|
||||
},
|
||||
Storage: &unifiedresources.StorageMeta{
|
||||
Type: "unraid-array",
|
||||
Platform: "unraid",
|
||||
Topology: "array",
|
||||
},
|
||||
},
|
||||
})
|
||||
adapter := unifiedresources.NewMonitorAdapter(registry)
|
||||
|
||||
frontend := convertResourcesForBroadcast(adapter.GetAll(), adapter)
|
||||
|
||||
var storagePayload struct {
|
||||
ID string `json:"id"`
|
||||
MetricsTarget struct {
|
||||
ResourceType string `json:"resourceType"`
|
||||
ResourceID string `json:"resourceId"`
|
||||
} `json:"metricsTarget"`
|
||||
}
|
||||
for _, resource := range frontend {
|
||||
if resource.ID != storageID {
|
||||
continue
|
||||
}
|
||||
encoded, err := json.Marshal(resource)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal storage frontend resource: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal(encoded, &storagePayload); err != nil {
|
||||
t.Fatalf("unmarshal storage frontend resource: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if storagePayload.ID != storageID {
|
||||
t.Fatalf("expected storage resource %q in frontend payload, got %q", storageID, storagePayload.ID)
|
||||
}
|
||||
if storagePayload.MetricsTarget.ResourceType != "storage" ||
|
||||
storagePayload.MetricsTarget.ResourceID != agentID+"/storage:unraid-array" {
|
||||
t.Fatalf("metrics target = %+v", storagePayload.MetricsTarget)
|
||||
}
|
||||
}
|
||||
|
||||
func hasFrontendResourceName(resources []models.ResourceFrontend, name string) bool {
|
||||
for _, resource := range resources {
|
||||
if resource.Name == name {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue