mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-25 21:20:00 +00:00
37454ef478
Reuse the canonical inventory exporter and report delivery workflow so operators can email or save a current organization-wide inventory weekly or monthly. Keep delivery checks, bounded retries and recovery states shared, with existing pricing and customer entitlements unchanged. Refresh the shared inventory for unattended reads and rebind reporting after monitor reload so schedules use the current infrastructure state.
166 lines
5.1 KiB
Go
166 lines
5.1 KiB
Go
package api
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
"github.com/rcourtman/pulse-go-rewrite/pkg/reporting"
|
|
)
|
|
|
|
// reportAvailabilityChangeLimit bounds how many timeline rows a single
|
|
// report subject pulls. Production resources record a handful of state
|
|
// transitions per month; a resource that exceeds this is flapping hard
|
|
// enough that the truncated window still tells the honest story.
|
|
const reportAvailabilityChangeLimit = 20000
|
|
|
|
// availabilityState classifies a recorded resource state for uptime math.
|
|
type availabilityState int
|
|
|
|
const (
|
|
availabilityStateUnobserved availabilityState = iota
|
|
availabilityStateUp
|
|
availabilityStateDown
|
|
)
|
|
|
|
// classifyAvailabilityState maps the canonical resource state vocabulary
|
|
// (online / warning / offline / unknown plus the synthetic "absent" emitted
|
|
// when a resource enters or leaves the registry) onto uptime semantics.
|
|
// Warning counts as up: the resource is reachable and serving, just
|
|
// unhealthy. Absent and unknown are unobserved - a monitoring gap is not an
|
|
// outage. Unrecognized future states default to up because a client-facing
|
|
// stability report claiming false downtime is worse than missing an exotic
|
|
// down state.
|
|
func classifyAvailabilityState(state string) availabilityState {
|
|
switch strings.ToLower(strings.TrimSpace(state)) {
|
|
case "", "absent", "unknown":
|
|
return availabilityStateUnobserved
|
|
case "offline":
|
|
return availabilityStateDown
|
|
default:
|
|
return availabilityStateUp
|
|
}
|
|
}
|
|
|
|
func availabilityChangeTime(change unifiedresources.ResourceChange) time.Time {
|
|
if change.OccurredAt != nil && !change.OccurredAt.IsZero() {
|
|
return *change.OccurredAt
|
|
}
|
|
return change.ObservedAt
|
|
}
|
|
|
|
// computeReportAvailability derives an availability summary for one report
|
|
// subject from its recorded state timeline. The walk reconstructs the state
|
|
// for every moment of [start, end]: the state before the first in-window
|
|
// transition is that transition's From; with no transitions at all the
|
|
// resource sat in currentState for the whole window (any change would have
|
|
// been journaled).
|
|
func computeReportAvailability(changes []unifiedresources.ResourceChange, currentState string, start, end time.Time) *reporting.AvailabilityInfo {
|
|
if !end.After(start) {
|
|
return nil
|
|
}
|
|
|
|
transitions := make([]unifiedresources.ResourceChange, 0, len(changes))
|
|
for _, change := range changes {
|
|
if change.Kind != unifiedresources.ChangeStateTransition {
|
|
continue
|
|
}
|
|
at := availabilityChangeTime(change)
|
|
if at.Before(start) || at.After(end) {
|
|
continue
|
|
}
|
|
transitions = append(transitions, change)
|
|
}
|
|
sort.SliceStable(transitions, func(i, j int) bool {
|
|
return availabilityChangeTime(transitions[i]).Before(availabilityChangeTime(transitions[j]))
|
|
})
|
|
|
|
initialState := currentState
|
|
if len(transitions) > 0 {
|
|
initialState = transitions[0].From
|
|
}
|
|
|
|
var up, down time.Duration
|
|
var downIncidents int
|
|
var longestOutage, currentOutage time.Duration
|
|
|
|
accumulate := func(state availabilityState, d time.Duration) {
|
|
if d <= 0 {
|
|
return
|
|
}
|
|
switch state {
|
|
case availabilityStateUp:
|
|
up += d
|
|
case availabilityStateDown:
|
|
down += d
|
|
currentOutage += d
|
|
}
|
|
if state != availabilityStateDown {
|
|
if currentOutage > longestOutage {
|
|
longestOutage = currentOutage
|
|
}
|
|
currentOutage = 0
|
|
}
|
|
}
|
|
|
|
cursor := start
|
|
state := classifyAvailabilityState(initialState)
|
|
for _, change := range transitions {
|
|
at := availabilityChangeTime(change)
|
|
accumulate(state, at.Sub(cursor))
|
|
next := classifyAvailabilityState(change.To)
|
|
if next == availabilityStateDown && state != availabilityStateDown {
|
|
downIncidents++
|
|
}
|
|
state = next
|
|
cursor = at
|
|
}
|
|
accumulate(state, end.Sub(cursor))
|
|
if currentOutage > longestOutage {
|
|
longestOutage = currentOutage
|
|
}
|
|
|
|
window := end.Sub(start)
|
|
observed := up + down
|
|
info := &reporting.AvailabilityInfo{
|
|
ObservedPercent: 100 * float64(observed) / float64(window),
|
|
TotalDowntime: down,
|
|
LongestOutage: longestOutage,
|
|
DownIncidents: downIncidents,
|
|
}
|
|
if observed > 0 {
|
|
info.UptimePercent = 100 * float64(up) / float64(observed)
|
|
}
|
|
return info
|
|
}
|
|
|
|
// resolveReportAvailability attaches the availability summary for the report
|
|
// subject. The change timeline is keyed by the canonical unified resource ID
|
|
// (not the metrics-target ID), so this runs against req.ResourceID.
|
|
func (h *ReportingHandlers) resolveReportAvailability(orgID string, req *reporting.MetricReportRequest, snapshot reportingEnrichmentSnapshot, start, end time.Time) {
|
|
manager := h.reportMonitorManager()
|
|
if manager == nil || req == nil {
|
|
return
|
|
}
|
|
|
|
currentState := ""
|
|
for i := range snapshot.Resources {
|
|
if snapshot.Resources[i].ID == req.ResourceID {
|
|
currentState = string(snapshot.Resources[i].Status)
|
|
break
|
|
}
|
|
}
|
|
if currentState == "" {
|
|
// Unknown to the unified registry: no timeline to report against.
|
|
return
|
|
}
|
|
|
|
monitor, err := manager.GetMonitor(orgID)
|
|
if err != nil || monitor == nil {
|
|
return
|
|
}
|
|
changes := monitor.RecentResourceChanges(req.ResourceID, start, reportAvailabilityChangeLimit)
|
|
req.Availability = computeReportAvailability(changes, currentState, start, end)
|
|
}
|