mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
fix(mock): backfill the metrics store in mock mode so reports get real history
Mock mode seeded rich in-memory chart history but passed a nil store to the seeder and the live tick, so the sqlite metrics store held almost nothing: vmware/truenas/docker resources got one row per poll from the platform ingestion paths and mock Proxmox guests got nothing at all (the unified sync skips every resource in mock mode). Performance reports rendered "Data Points: 0" for PVE guests and "Samples: 1" elsewhere, with no charts. Re-enable store seeding behind an explicit PULSE_MOCK_SEED_METRICS_STORE opt-in that scripts/hot-dev.sh and scripts/toggle-mock.sh export exactly where they point PULSE_DATA_DIR at the isolated tmp/mock-data dir. Without the opt-in (a production install flipping PULSE_MOCK_MODE on its real data dir) the store stays untouched, which is what the old nil guard protected. Replace the dormant dense seeding policy (every in-memory timestamp written to both hourly and daily tiers, ~11M rows at current fixture scale) with tier-correct backfill driven by the deterministic mock.SampleMetric runtime: daily 30d at 4h spacing, hourly 7d at 2h, minute 24h at 15m, raw left to the live tick (now also store-connected under the opt-in). Timestamps sit on the spacing grid and a new Store.MaxTimestampsForTier coverage query fills only the gap since the previous boot, so restarts neither duplicate rows nor re-pay the seed: a fresh seed wrote 264,960 rows in 8.4s (71MB) and a restart wrote only the gap plus fixture series whose mock IDs are not boot-stable (k8s pod names, ceph FSID, a pre-existing generator defect). Verified live in mock mode: 30-day PDF reports for a mock PVE VM (checkout-web-01), a vSphere VM, the TrueNAS host, and a Docker container now show 1260/1260/1260/540 data points with rendered time-series charts.
This commit is contained in:
parent
8ce834cc59
commit
f136b1ef59
8 changed files with 487 additions and 96 deletions
|
|
@ -158,6 +158,24 @@ type WriteMetric struct {
|
|||
Tier Tier
|
||||
}
|
||||
|
||||
// SeriesKey identifies one stored metric series in the normalized form the
|
||||
// write path persists (lower-cased resource/metric types, trimmed id).
|
||||
type SeriesKey struct {
|
||||
ResourceType string
|
||||
ResourceID string
|
||||
MetricType string
|
||||
}
|
||||
|
||||
// NormalizedSeriesKey builds the SeriesKey the write path would store for the
|
||||
// given identifiers, so callers can match MaxTimestampsForTier results.
|
||||
func NormalizedSeriesKey(resourceType, resourceID, metricType string) SeriesKey {
|
||||
return SeriesKey{
|
||||
ResourceType: normalizeMetricResourceType(resourceType),
|
||||
ResourceID: normalizeMetricIdentifier(resourceID),
|
||||
MetricType: normalizeMetricType(metricType),
|
||||
}
|
||||
}
|
||||
|
||||
type maintenanceRequest struct {
|
||||
run func()
|
||||
done chan struct{}
|
||||
|
|
@ -1707,6 +1725,33 @@ func (s *Store) getMaxTimestampForTier(tier Tier) (int64, bool) {
|
|||
return maxTs.Int64, true
|
||||
}
|
||||
|
||||
// MaxTimestampsForTier returns the newest stored timestamp for every series
|
||||
// present in the given tier. Backfill seeders use it to write only the gap
|
||||
// between existing coverage and now instead of re-writing whole windows.
|
||||
func (s *Store) MaxTimestampsForTier(tier Tier) (map[SeriesKey]time.Time, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT resource_type, resource_id, metric_type, MAX(timestamp)
|
||||
FROM metrics
|
||||
WHERE tier = ?
|
||||
GROUP BY resource_type, resource_id, metric_type
|
||||
`, string(tier))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query metrics coverage for tier %q: %w", tier, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
coverage := make(map[SeriesKey]time.Time)
|
||||
for rows.Next() {
|
||||
var key SeriesKey
|
||||
var ts int64
|
||||
if err := rows.Scan(&key.ResourceType, &key.ResourceID, &key.MetricType, &ts); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan metrics coverage row: %w", err)
|
||||
}
|
||||
coverage[key] = time.Unix(ts, 0).UTC()
|
||||
}
|
||||
return coverage, rows.Err()
|
||||
}
|
||||
|
||||
// runRetention deletes data older than retention period
|
||||
func (s *Store) runRetention() {
|
||||
start := time.Now()
|
||||
|
|
|
|||
49
pkg/metrics/store_series_coverage_test.go
Normal file
49
pkg/metrics/store_series_coverage_test.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMaxTimestampsForTier(t *testing.T) {
|
||||
store, err := NewStore(DefaultConfig(t.TempDir()))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create metrics store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
base := time.Now().UTC().Truncate(time.Hour)
|
||||
store.WriteBatchSync([]WriteMetric{
|
||||
{ResourceType: "vm", ResourceID: "vm-1", MetricType: "cpu", Value: 1, Timestamp: base.Add(-2 * time.Hour), Tier: TierHourly},
|
||||
{ResourceType: "vm", ResourceID: "vm-1", MetricType: "cpu", Value: 2, Timestamp: base, Tier: TierHourly},
|
||||
{ResourceType: " VM ", ResourceID: " vm-2 ", MetricType: " CPU ", Value: 3, Timestamp: base.Add(-time.Hour), Tier: TierHourly},
|
||||
{ResourceType: "vm", ResourceID: "vm-1", MetricType: "cpu", Value: 4, Timestamp: base.Add(time.Hour), Tier: TierRaw},
|
||||
})
|
||||
|
||||
coverage, err := store.MaxTimestampsForTier(TierHourly)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read hourly coverage: %v", err)
|
||||
}
|
||||
if len(coverage) != 2 {
|
||||
t.Fatalf("expected 2 hourly series, got %d: %v", len(coverage), coverage)
|
||||
}
|
||||
if got := coverage[NormalizedSeriesKey("vm", "vm-1", "cpu")]; !got.Equal(base) {
|
||||
t.Fatalf("expected vm-1 hourly coverage at %s, got %s", base, got)
|
||||
}
|
||||
// Coverage keys are stored normalized, so messy caller identifiers must
|
||||
// resolve through NormalizedSeriesKey.
|
||||
if got := coverage[NormalizedSeriesKey(" VM ", " vm-2 ", " CPU ")]; !got.Equal(base.Add(-time.Hour)) {
|
||||
t.Fatalf("expected normalized vm-2 hourly coverage at %s, got %s", base.Add(-time.Hour), got)
|
||||
}
|
||||
|
||||
rawCoverage, err := store.MaxTimestampsForTier(TierRaw)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read raw coverage: %v", err)
|
||||
}
|
||||
if len(rawCoverage) != 1 {
|
||||
t.Fatalf("expected 1 raw series, got %d: %v", len(rawCoverage), rawCoverage)
|
||||
}
|
||||
if got := rawCoverage[NormalizedSeriesKey("vm", "vm-1", "cpu")]; !got.Equal(base.Add(time.Hour)) {
|
||||
t.Fatalf("expected vm-1 raw coverage at %s, got %s", base.Add(time.Hour), got)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue