feat: Monitor grath zoom

This commit is contained in:
2026-08-24 10:58:50 +00:00
parent 2fab784ba7
commit 22b99ff895
11 changed files with 395 additions and 14 deletions
@@ -36,6 +36,7 @@ var ScopedCollections = []string{
"monitors",
"incidents",
"monitor_rollups",
"monitor_samples",
"notification_channels",
"console_sessions",
"audit_logs",
+37
View File
@@ -221,6 +221,7 @@ func DeleteMonitor(instanceID, monitorID string) error {
}
db.Col("incidents").DeleteMany(ctx, bson.M{"monitor_id": monitorID, "instance_id": instanceID})
db.Col("monitor_rollups").DeleteMany(ctx, bson.M{"monitor_id": monitorID, "instance_id": instanceID})
db.Col("monitor_samples").DeleteMany(ctx, bson.M{"monitor_id": monitorID, "instance_id": instanceID})
return nil
}
@@ -258,6 +259,31 @@ func UptimeRollups(instanceID, monitorID string, since time.Time) ([]models.Roll
return out, nil
}
// MaxMonitorSamples bounds one range read. At the 10s floor, 48h is 17,280
// checks; the chart buckets them anyway, so a cap costs nothing visible and
// stops one monitor pulling a megabyte of JSON per poll.
const MaxMonitorSamples = 6000
// MonitorSamples returns individual check results since a point in time,
// oldest first. Samples older than MonitorSampleTTL have expired, so an early
// `since` silently returns a shorter window rather than an error — the caller
// draws the gap.
func MonitorSamples(instanceID, monitorID string, since time.Time) ([]models.MonitorSample, error) {
ctx, cancel := monCtx()
defer cancel()
cur, err := db.Col("monitor_samples").Find(ctx,
bson.M{"monitor_id": monitorID, "instance_id": instanceID, "at": bson.M{"$gte": since}},
options.Find().SetSort(bson.M{"at": 1}).SetLimit(MaxMonitorSamples))
if err != nil {
return nil, err
}
var out []models.MonitorSample
if err := cur.All(ctx, &out); err != nil {
return nil, err
}
return out, nil
}
func IngestResult(instanceID, runner, monitorID string, res checker.Result) error {
if instanceID == "" {
return errors.New("instance id required")
@@ -328,6 +354,17 @@ func ingestResult(instanceID, runner, monitorID string, res checker.Result) erro
up = 1
}
/* The sample is the same result at full resolution, expiring by TTL. It is
written next to the rollup rather than instead of it: the rollup is what
survives, the sample is what the sub-hour views read. */
db.Col("monitor_samples").InsertOne(ctx, models.MonitorSample{
InstanceID: m.InstanceID,
MonitorID: monitorID,
At: now,
Up: res.Up,
LatencyMs: res.LatencyMs,
})
db.Col("monitor_rollups").UpdateOne(ctx,
bson.M{"monitor_id": monitorID, "period_start": bucket},
bson.M{
@@ -0,0 +1,42 @@
package services
import (
"context"
"log"
"time"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/db"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
// MonitorSampleTTL is how long an individual check result is kept.
//
// It matches the longest range the chart draws from samples rather than the
// longest range it draws at all: 24h and 48h come from the hourly rollups,
// which are permanent. Keeping samples past the window that reads them would
// only grow the collection.
const MonitorSampleTTL = 48 * time.Hour
// EnsureMonitorSampleIndexes declares the sample range index and its TTL.
//
// Warn rather than fatal, like the other history indexes — but note the TTL is
// not an optimisation: without it nothing ever removes a sample, and the
// collection grows at the fleet's total check rate forever. A boot that logs
// this warning needs following up.
func EnsureMonitorSampleIndexes() error {
ctx := context.Background()
idx := []mongo.IndexModel{
// Every read is a range scan over this key.
{Keys: bson.D{{Key: "instance_id", Value: 1}, {Key: "monitor_id", Value: 1}, {Key: "at", Value: 1}}},
// Expiry is Mongo's job: a sweeper would be another leader-scoped loop
// doing what the server already does for free.
{Keys: bson.D{{Key: "at", Value: 1}}, Options: options.Index().SetExpireAfterSeconds(int32(MonitorSampleTTL.Seconds()))},
}
if _, err := db.Col("monitor_samples").Indexes().CreateMany(ctx, idx); err != nil {
log.Printf("warning: monitor_samples indexes: %v", err)
}
return nil
}