From dd435a8eafa604b0cb5ed9355adb17fdca48aba7 Mon Sep 17 00:00:00 2001 From: TwiN Date: Sun, 11 Aug 2024 22:40:19 -0400 Subject: [PATCH] feat(storage): Support 30d badges (#836) * feat(storage): Add support for 30d uptime badge Fix #714 * Fix typo * Fix test * Fix typo * Improve implementation * Add check in existing test * Add extra test to ensure functionality works * Add support for 30d response time chart too --- README.md | 4 +- api/badge.go | 16 +++- api/chart.go | 10 ++- api/chart_test.go | 5 ++ storage/store/memory/uptime.go | 12 +-- storage/store/memory/uptime_test.go | 4 +- storage/store/sql/sql.go | 117 +++++++++++++++++++++++- storage/store/sql/sql_test.go | 135 ++++++++++++++++++++++++++-- 8 files changed, 276 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 8004653d..bf9ac39b 100644 --- a/README.md +++ b/README.md @@ -2127,7 +2127,7 @@ The path to generate a badge is the following: /api/v1/endpoints/{key}/uptimes/{duration}/badge.svg ``` Where: -- `{duration}` is `7d`, `24h` or `1h` +- `{duration}` is `30d` (alpha), `7d`, `24h` or `1h` - `{key}` has the pattern `_` in which both variables have ` `, `/`, `_`, `,` and `.` replaced by `-`. For instance, if you want the uptime during the last 24 hours from the endpoint `frontend` in the group `core`, @@ -2192,7 +2192,7 @@ The endpoint to generate a badge is the following: /api/v1/endpoints/{key}/response-times/{duration}/badge.svg ``` Where: -- `{duration}` is `7d`, `24h` or `1h` +- `{duration}` is `30d` (alpha), `7d`, `24h` or `1h` - `{key}` has the pattern `_` in which both variables have ` `, `/`, `_`, `,` and `.` replaced by `-`. diff --git a/api/badge.go b/api/badge.go index 66a8a36b..e2ed3f46 100644 --- a/api/badge.go +++ b/api/badge.go @@ -37,11 +37,13 @@ var ( // UptimeBadge handles the automatic generation of badge based on the group name and endpoint name passed. // -// Valid values for :duration -> 7d, 24h, 1h +// Valid values for :duration -> 30d, 7d, 24h, 1h func UptimeBadge(c *fiber.Ctx) error { duration := c.Params("duration") var from time.Time switch duration { + case "30d": + from = time.Now().Add(-30 * 24 * time.Hour) case "7d": from = time.Now().Add(-7 * 24 * time.Hour) case "24h": @@ -49,7 +51,7 @@ func UptimeBadge(c *fiber.Ctx) error { case "1h": from = time.Now().Add(-2 * time.Hour) // Because uptime metrics are stored by hour, we have to cheat a little default: - return c.Status(400).SendString("Durations supported: 7d, 24h, 1h") + return c.Status(400).SendString("Durations supported: 30d, 7d, 24h, 1h") } key := c.Params("key") uptime, err := store.Get().GetUptimeByKey(key, from, time.Now()) @@ -69,12 +71,14 @@ func UptimeBadge(c *fiber.Ctx) error { // ResponseTimeBadge handles the automatic generation of badge based on the group name and endpoint name passed. // -// Valid values for :duration -> 7d, 24h, 1h +// Valid values for :duration -> 30d, 7d, 24h, 1h func ResponseTimeBadge(cfg *config.Config) fiber.Handler { return func(c *fiber.Ctx) error { duration := c.Params("duration") var from time.Time switch duration { + case "30d": + from = time.Now().Add(-30 * 24 * time.Hour) case "7d": from = time.Now().Add(-7 * 24 * time.Hour) case "24h": @@ -82,7 +86,7 @@ func ResponseTimeBadge(cfg *config.Config) fiber.Handler { case "1h": from = time.Now().Add(-2 * time.Hour) // Because response time metrics are stored by hour, we have to cheat a little default: - return c.Status(400).SendString("Durations supported: 7d, 24h, 1h") + return c.Status(400).SendString("Durations supported: 30d, 7d, 24h, 1h") } key := c.Params("key") averageResponseTime, err := store.Get().GetAverageResponseTimeByKey(key, from, time.Now()) @@ -161,6 +165,8 @@ func HealthBadgeShields(c *fiber.Ctx) error { func generateUptimeBadgeSVG(duration string, uptime float64) []byte { var labelWidth, valueWidth, valueWidthAdjustment int switch duration { + case "30d": + labelWidth = 70 case "7d": labelWidth = 65 case "24h": @@ -227,6 +233,8 @@ func getBadgeColorFromUptime(uptime float64) string { func generateResponseTimeBadgeSVG(duration string, averageResponseTime int, key string, cfg *config.Config) []byte { var labelWidth, valueWidth int switch duration { + case "30d": + labelWidth = 110 case "7d": labelWidth = 105 case "24h": diff --git a/api/chart.go b/api/chart.go index 3c8a07ae..ff118e1d 100644 --- a/api/chart.go +++ b/api/chart.go @@ -32,14 +32,18 @@ var ( func ResponseTimeChart(c *fiber.Ctx) error { duration := c.Params("duration") + chartTimestampFormatter := chart.TimeValueFormatterWithFormat(timeFormat) var from time.Time switch duration { + case "30d": + from = time.Now().Truncate(time.Hour).Add(-30 * 24 * time.Hour) + chartTimestampFormatter = chart.TimeDateValueFormatter case "7d": - from = time.Now().Truncate(time.Hour).Add(-24 * 7 * time.Hour) + from = time.Now().Truncate(time.Hour).Add(-7 * 24 * time.Hour) case "24h": from = time.Now().Truncate(time.Hour).Add(-24 * time.Hour) default: - return c.Status(400).SendString("Durations supported: 7d, 24h") + return c.Status(400).SendString("Durations supported: 30d, 7d, 24h") } hourlyAverageResponseTime, err := store.Get().GetHourlyAverageResponseTimeByKey(c.Params("key"), from, time.Now()) if err != nil { @@ -88,7 +92,7 @@ func ResponseTimeChart(c *fiber.Ctx) error { Width: 1280, Height: 300, XAxis: chart.XAxis{ - ValueFormatter: chart.TimeValueFormatterWithFormat(timeFormat), + ValueFormatter: chartTimestampFormatter, GridMajorStyle: gridStyle, GridMinorStyle: gridStyle, Style: axisStyle, diff --git a/api/chart_test.go b/api/chart_test.go index 0c3c769e..2a699a5b 100644 --- a/api/chart_test.go +++ b/api/chart_test.go @@ -49,6 +49,11 @@ func TestResponseTimeChart(t *testing.T) { Path: "/api/v1/endpoints/core_frontend/response-times/7d/chart.svg", ExpectedCode: http.StatusOK, }, + { + Name: "chart-response-time-30d", + Path: "/api/v1/endpoints/core_frontend/response-times/30d/chart.svg", + ExpectedCode: http.StatusOK, + }, { Name: "chart-response-time-with-invalid-duration", Path: "/api/v1/endpoints/core_backend/response-times/3d/chart.svg", diff --git a/storage/store/memory/uptime.go b/storage/store/memory/uptime.go index 29259f6d..93322359 100644 --- a/storage/store/memory/uptime.go +++ b/storage/store/memory/uptime.go @@ -7,8 +7,8 @@ import ( ) const ( - numberOfHoursInTenDays = 10 * 24 - sevenDays = 7 * 24 * time.Hour + uptimeCleanUpThreshold = 32 * 24 + uptimeRetention = 30 * 24 * time.Hour ) // processUptimeAfterResult processes the result by extracting the relevant from the result and recalculating the uptime @@ -30,10 +30,10 @@ func processUptimeAfterResult(uptime *endpoint.Uptime, result *endpoint.Result) hourlyStats.TotalExecutionsResponseTime += uint64(result.Duration.Milliseconds()) // Clean up only when we're starting to have too many useless keys // Note that this is only triggered when there are more entries than there should be after - // 10 days, despite the fact that we are deleting everything that's older than 7 days. - // This is to prevent re-iterating on every `processUptimeAfterResult` as soon as the uptime has been logged for 7 days. - if len(uptime.HourlyStatistics) > numberOfHoursInTenDays { - sevenDaysAgo := time.Now().Add(-(sevenDays + time.Hour)).Unix() + // 32 days, despite the fact that we are deleting everything that's older than 30 days. + // This is to prevent re-iterating on every `processUptimeAfterResult` as soon as the uptime has been logged for 30 days. + if len(uptime.HourlyStatistics) > uptimeCleanUpThreshold { + sevenDaysAgo := time.Now().Add(-(uptimeRetention + time.Hour)).Unix() for hourlyUnixTimestamp := range uptime.HourlyStatistics { if sevenDaysAgo > hourlyUnixTimestamp { delete(uptime.HourlyStatistics, hourlyUnixTimestamp) diff --git a/storage/store/memory/uptime_test.go b/storage/store/memory/uptime_test.go index 01670ad3..3d9f36c1 100644 --- a/storage/store/memory/uptime_test.go +++ b/storage/store/memory/uptime_test.go @@ -51,8 +51,8 @@ func TestAddResultUptimeIsCleaningUpAfterItself(t *testing.T) { timestamp := now.Add(-12 * 24 * time.Hour) for timestamp.Unix() <= now.Unix() { AddResult(status, &endpoint.Result{Timestamp: timestamp, Success: true}) - if len(status.Uptime.HourlyStatistics) > numberOfHoursInTenDays { - t.Errorf("At no point in time should there be more than %d entries in status.SuccessfulExecutionsPerHour, but there are %d", numberOfHoursInTenDays, len(status.Uptime.HourlyStatistics)) + if len(status.Uptime.HourlyStatistics) > uptimeCleanUpThreshold { + t.Errorf("At no point in time should there be more than %d entries in status.SuccessfulExecutionsPerHour, but there are %d", uptimeCleanUpThreshold, len(status.Uptime.HourlyStatistics)) } // Simulate endpoint with an interval of 3 minutes timestamp = timestamp.Add(3 * time.Minute) diff --git a/storage/store/sql/sql.go b/storage/store/sql/sql.go index 7cfa9b5f..a2a6791d 100644 --- a/storage/store/sql/sql.go +++ b/storage/store/sql/sql.go @@ -28,11 +28,13 @@ const ( // for aesthetic purposes, I deemed it wasn't worth the performance impact of yet another one-to-many table. arraySeparator = "|~|" - uptimeCleanUpThreshold = 10 * 24 * time.Hour // Maximum uptime age before triggering a cleanup eventsCleanUpThreshold = common.MaximumNumberOfEvents + 10 // Maximum number of events before triggering a cleanup resultsCleanUpThreshold = common.MaximumNumberOfResults + 10 // Maximum number of results before triggering a cleanup - uptimeRetention = 7 * 24 * time.Hour + uptimeTotalEntriesMergeThreshold = 100 // Maximum number of uptime entries before triggering a merge + uptimeAgeCleanUpThreshold = 32 * 24 * time.Hour // Maximum uptime age before triggering a cleanup + uptimeRetention = 30 * 24 * time.Hour // Minimum duration that must be kept to operate as intended + uptimeHourlyBuffer = 48 * time.Hour // Number of hours to buffer from now when determining which hourly uptime entries can be merged into daily uptime entries cacheTTL = 10 * time.Minute ) @@ -322,12 +324,27 @@ func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error { if err = s.updateEndpointUptime(tx, endpointID, result); err != nil { log.Printf("[sql.Insert] Failed to update uptime for endpoint with key=%s: %s", ep.Key(), err.Error()) } - // Clean up old uptime entries + // Merge hourly uptime entries that can be merged into daily entries and clean up old uptime entries + numberOfUptimeEntries, err := s.getNumberOfUptimeEntriesByEndpointID(tx, endpointID) + if err != nil { + log.Printf("[sql.Insert] Failed to retrieve total number of uptime entries for endpoint with key=%s: %s", ep.Key(), err.Error()) + } else { + // Merge older hourly uptime entries into daily uptime entries if we have more than uptimeTotalEntriesMergeThreshold + if numberOfUptimeEntries >= uptimeTotalEntriesMergeThreshold { + log.Printf("[sql.Insert] Merging hourly uptime entries for endpoint with key=%s; This is a lot of work, it shouldn't happen too often", ep.Key()) + if err = s.mergeHourlyUptimeEntriesOlderThanMergeThresholdIntoDailyUptimeEntries(tx, endpointID); err != nil { + log.Printf("[sql.Insert] Failed to merge hourly uptime entries for endpoint with key=%s: %s", ep.Key(), err.Error()) + } + } + } + // Clean up outdated uptime entries + // In most cases, this would be handled by mergeHourlyUptimeEntriesOlderThanMergeThresholdIntoDailyUptimeEntries, + // but if Gatus was temporarily shut down, we might have some old entries that need to be cleaned up ageOfOldestUptimeEntry, err := s.getAgeOfOldestEndpointUptimeEntry(tx, endpointID) if err != nil { log.Printf("[sql.Insert] Failed to retrieve oldest endpoint uptime entry for endpoint with key=%s: %s", ep.Key(), err.Error()) } else { - if ageOfOldestUptimeEntry > uptimeCleanUpThreshold { + if ageOfOldestUptimeEntry > uptimeAgeCleanUpThreshold { if err = s.deleteOldUptimeEntries(tx, endpointID, time.Now().Add(-(uptimeRetention + time.Hour))); err != nil { log.Printf("[sql.Insert] Failed to delete old uptime entries for endpoint with key=%s: %s", ep.Key(), err.Error()) } @@ -865,6 +882,12 @@ func (s *Store) getNumberOfResultsByEndpointID(tx *sql.Tx, endpointID int64) (in return numberOfResults, err } +func (s *Store) getNumberOfUptimeEntriesByEndpointID(tx *sql.Tx, endpointID int64) (int64, error) { + var numberOfUptimeEntries int64 + err := tx.QueryRow("SELECT COUNT(1) FROM endpoint_uptimes WHERE endpoint_id = $1", endpointID).Scan(&numberOfUptimeEntries) + return numberOfUptimeEntries, err +} + func (s *Store) getAgeOfOldestEndpointUptimeEntry(tx *sql.Tx, endpointID int64) (time.Duration, error) { rows, err := tx.Query( ` @@ -948,6 +971,92 @@ func (s *Store) deleteOldUptimeEntries(tx *sql.Tx, endpointID int64, maxAge time return err } +// mergeHourlyUptimeEntriesOlderThanMergeThresholdIntoDailyUptimeEntries merges all hourly uptime entries older than +// uptimeHourlyMergeThreshold from now into daily uptime entries by summing all hourly entries of the same day into a +// single entry. +// +// This effectively limits the number of uptime entries to (48+(n-2)) where 48 is for the first 48 entries with hourly +// entries (defined by uptimeHourlyBuffer) and n is the number of days for all entries older than 48 hours. +// Supporting 30d of entries would then result in far less than 24*30=720 entries. +func (s *Store) mergeHourlyUptimeEntriesOlderThanMergeThresholdIntoDailyUptimeEntries(tx *sql.Tx, endpointID int64) error { + // Calculate timestamp of the first full day of uptime entries that would not impact the uptime calculation for 24h badges + // The logic is that once at least 48 hours passed, we: + // - No longer need to worry about keeping hourly entries + // - Don't have to worry about new hourly entries being inserted, as the day has already passed + // which implies that no matter at what hour of the day we are, any timestamp + 48h floored to the current day + // will never impact the 24h uptime badge calculation + now := time.Now() + minThreshold := now.Add(-uptimeHourlyBuffer) + minThreshold = time.Date(minThreshold.Year(), minThreshold.Month(), minThreshold.Day(), 0, 0, 0, 0, minThreshold.Location()) + maxThreshold := now.Add(-uptimeRetention) + // Get all uptime entries older than uptimeHourlyMergeThreshold + rows, err := tx.Query( + ` + SELECT hour_unix_timestamp, total_executions, successful_executions, total_response_time + FROM endpoint_uptimes + WHERE endpoint_id = $1 + AND hour_unix_timestamp < $2 + AND hour_unix_timestamp >= $3 + `, + endpointID, + minThreshold.Unix(), + maxThreshold.Unix(), + ) + if err != nil { + return err + } + type Entry struct { + totalExecutions int + successfulExecutions int + totalResponseTime int + } + dailyEntries := make(map[int64]*Entry) + for rows.Next() { + var unixTimestamp int64 + entry := Entry{} + if err = rows.Scan(&unixTimestamp, &entry.totalExecutions, &entry.successfulExecutions, &entry.totalResponseTime); err != nil { + return err + } + timestamp := time.Unix(unixTimestamp, 0) + unixTimestampFlooredAtDay := time.Date(timestamp.Year(), timestamp.Month(), timestamp.Day(), 0, 0, 0, 0, timestamp.Location()).Unix() + if dailyEntry := dailyEntries[unixTimestampFlooredAtDay]; dailyEntry == nil { + dailyEntries[unixTimestampFlooredAtDay] = &entry + } else { + dailyEntries[unixTimestampFlooredAtDay].totalExecutions += entry.totalExecutions + dailyEntries[unixTimestampFlooredAtDay].successfulExecutions += entry.successfulExecutions + dailyEntries[unixTimestampFlooredAtDay].totalResponseTime += entry.totalResponseTime + } + } + // Delete older hourly uptime entries + _, err = tx.Exec("DELETE FROM endpoint_uptimes WHERE endpoint_id = $1 AND hour_unix_timestamp < $2", endpointID, minThreshold.Unix()) + if err != nil { + return err + } + // Insert new daily uptime entries + for unixTimestamp, entry := range dailyEntries { + _, err = tx.Exec( + ` + INSERT INTO endpoint_uptimes (endpoint_id, hour_unix_timestamp, total_executions, successful_executions, total_response_time) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT(endpoint_id, hour_unix_timestamp) DO UPDATE SET + total_executions = $3, + successful_executions = $4, + total_response_time = $5 + `, + endpointID, + unixTimestamp, + entry.totalExecutions, + entry.successfulExecutions, + entry.totalResponseTime, + ) + if err != nil { + return err + } + } + // TODO: Find a way to ignore entries that were already merged? + return nil +} + func generateCacheKey(endpointKey string, p *paging.EndpointStatusParams) string { return fmt.Sprintf("%s-%d-%d-%d-%d", endpointKey, p.EventsPage, p.EventsPageSize, p.ResultsPage, p.ResultsPageSize) } diff --git a/storage/store/sql/sql_test.go b/storage/store/sql/sql_test.go index b00eee28..7261c8fa 100644 --- a/storage/store/sql/sql_test.go +++ b/storage/store/sql/sql_test.go @@ -2,6 +2,7 @@ package sql import ( "errors" + "fmt" "testing" "time" @@ -132,18 +133,18 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) { } // Since this is one hour before reaching the clean up threshold, the oldest entry should now be this one - store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-(uptimeCleanUpThreshold - time.Hour)), Success: true}) + store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-(uptimeAgeCleanUpThreshold - time.Hour)), Success: true}) tx, _ = store.db.Begin() oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1) _ = tx.Commit() - if oldest.Truncate(time.Hour) != uptimeCleanUpThreshold-time.Hour { - t.Errorf("oldest endpoint uptime entry should've been ~%s hours old, was %s", uptimeCleanUpThreshold-time.Hour, oldest) + if oldest.Truncate(time.Hour) != uptimeAgeCleanUpThreshold-time.Hour { + t.Errorf("oldest endpoint uptime entry should've been ~%s hours old, was %s", uptimeAgeCleanUpThreshold-time.Hour, oldest) } - // Since this entry is after the uptimeCleanUpThreshold, both this entry as well as the previous + // Since this entry is after the uptimeAgeCleanUpThreshold, both this entry as well as the previous // one should be deleted since they both surpass uptimeRetention - store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-(uptimeCleanUpThreshold + time.Hour)), Success: true}) + store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-(uptimeAgeCleanUpThreshold + time.Hour)), Success: true}) tx, _ = store.db.Begin() oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1) @@ -153,8 +154,128 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) { } } +func TestStore_HourlyUptimeEntriesAreMergedIntoDailyUptimeEntriesProperly(t *testing.T) { + store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_HourlyUptimeEntriesAreMergedIntoDailyUptimeEntriesProperly.db", false) + defer store.Close() + now := time.Now().Truncate(time.Hour) + now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location()) + + scenarios := []struct { + numberOfHours int + expectedMaxUptimeEntries int64 + }{ + {numberOfHours: 1, expectedMaxUptimeEntries: 1}, + {numberOfHours: 10, expectedMaxUptimeEntries: 10}, + {numberOfHours: 50, expectedMaxUptimeEntries: 50}, + {numberOfHours: 75, expectedMaxUptimeEntries: 75}, + {numberOfHours: 99, expectedMaxUptimeEntries: 99}, + {numberOfHours: 150, expectedMaxUptimeEntries: 100}, + {numberOfHours: 300, expectedMaxUptimeEntries: 100}, + {numberOfHours: 768, expectedMaxUptimeEntries: 100}, // 32 days (in hours), which means anything beyond that won't be persisted anyway + {numberOfHours: 1000, expectedMaxUptimeEntries: 100}, + } + // Note that is not technically an accurate real world representation, because uptime entries are always added in + // the present, while this test is inserting results from the past to simulate long term uptime entries. + // Since we want to test the behavior and not the test itself, this is a "best effort" approach. + for _, scenario := range scenarios { + t.Run(fmt.Sprintf("num-hours-%d-expected-max-entries-%d", scenario.numberOfHours, scenario.expectedMaxUptimeEntries), func(t *testing.T) { + for i := scenario.numberOfHours; i > 0; i-- { + //fmt.Printf("i: %d (%s)\n", i, now.Add(-time.Duration(i)*time.Hour)) + // Create an uptime entry + err := store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-time.Duration(i) * time.Hour), Success: true}) + if err != nil { + t.Log(err) + } + //// DEBUGGING: check number of uptime entries for endpoint + //tx, _ := store.db.Begin() + //numberOfUptimeEntriesForEndpoint, err := store.getNumberOfUptimeEntriesByEndpointID(tx, 1) + //if err != nil { + // t.Log(err) + //} + //_ = tx.Commit() + //t.Logf("i=%d; numberOfHours=%d; There are currently %d uptime entries for endpointID=%d", i, scenario.numberOfHours, numberOfUptimeEntriesForEndpoint, 1) + } + // check number of uptime entries for endpoint + tx, _ := store.db.Begin() + numberOfUptimeEntriesForEndpoint, err := store.getNumberOfUptimeEntriesByEndpointID(tx, 1) + if err != nil { + t.Log(err) + } + _ = tx.Commit() + //t.Logf("numberOfHours=%d; There are currently %d uptime entries for endpointID=%d", scenario.numberOfHours, numberOfUptimeEntriesForEndpoint, 1) + if scenario.expectedMaxUptimeEntries < numberOfUptimeEntriesForEndpoint { + t.Errorf("expected %d (uptime entries) to be smaller than %d", numberOfUptimeEntriesForEndpoint, scenario.expectedMaxUptimeEntries) + } + store.Clear() + }) + } +} + +func TestStore_getEndpointUptime(t *testing.T) { + store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertCleansUpEventsAndResultsProperly.db", false) + defer store.Clear() + defer store.Close() + // Add 768 hourly entries (32 days) + // Daily entries should be merged from hourly entries automatically + for i := 768; i > 0; i-- { + err := store.Insert(&testEndpoint, &endpoint.Result{Timestamp: time.Now().Add(-time.Duration(i) * time.Hour), Duration: time.Second, Success: true}) + if err != nil { + t.Log(err) + } + } + // Check the number of uptime entries + tx, _ := store.db.Begin() + numberOfUptimeEntriesForEndpoint, err := store.getNumberOfUptimeEntriesByEndpointID(tx, 1) + if err != nil { + t.Log(err) + } + if numberOfUptimeEntriesForEndpoint < 20 || numberOfUptimeEntriesForEndpoint > 200 { + t.Errorf("expected number of uptime entries to be between 20 and 200, got %d", numberOfUptimeEntriesForEndpoint) + } + // Retrieve uptime for the past 30d + uptime, avgResponseTime, err := store.getEndpointUptime(tx, 1, time.Now().Add(-(30 * 24 * time.Hour)), time.Now()) + if err != nil { + t.Log(err) + } + _ = tx.Commit() + if avgResponseTime != time.Second { + t.Errorf("expected average response time to be %s, got %s", time.Second, avgResponseTime) + } + if uptime != 1 { + t.Errorf("expected uptime to be 1, got %f", uptime) + } + // Add a new unsuccessful result, which should impact the uptime + err = store.Insert(&testEndpoint, &endpoint.Result{Timestamp: time.Now(), Duration: time.Second, Success: false}) + if err != nil { + t.Log(err) + } + // Retrieve uptime for the past 30d + tx, _ = store.db.Begin() + uptime, _, err = store.getEndpointUptime(tx, 1, time.Now().Add(-(30 * 24 * time.Hour)), time.Now()) + if err != nil { + t.Log(err) + } + _ = tx.Commit() + if uptime == 1 { + t.Errorf("expected uptime to be less than 1, got %f", uptime) + } + // Retrieve uptime for the past 30d, but excluding the last 24h + // This is not a real use case as there is no way for users to exclude the last 24h, but this is a great way + // to ensure that hourly merging works as intended + tx, _ = store.db.Begin() + uptimeExcludingLast24h, _, err := store.getEndpointUptime(tx, 1, time.Now().Add(-(30 * 24 * time.Hour)), time.Now().Add(-24*time.Hour)) + if err != nil { + t.Log(err) + } + _ = tx.Commit() + if uptimeExcludingLast24h == uptime { + t.Error("expected uptimeExcludingLast24h to to be different from uptime, got") + } +} + func TestStore_InsertCleansUpEventsAndResultsProperly(t *testing.T) { store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertCleansUpEventsAndResultsProperly.db", false) + defer store.Clear() defer store.Close() for i := 0; i < resultsCleanUpThreshold+eventsCleanUpThreshold; i++ { store.Insert(&testEndpoint, &testSuccessfulResult) @@ -167,7 +288,6 @@ func TestStore_InsertCleansUpEventsAndResultsProperly(t *testing.T) { t.Errorf("number of events shouldn't have exceeded %d, reached %d", eventsCleanUpThreshold, len(ss.Events)) } } - store.Clear() } func TestStore_InsertWithCaching(t *testing.T) { @@ -218,6 +338,9 @@ func TestStore_Persistence(t *testing.T) { if uptime, _ := store.GetUptimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour*24*7), time.Now()); uptime != 0.5 { t.Errorf("the uptime over the past 7d should've been 0.5, got %f", uptime) } + if uptime, _ := store.GetUptimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour*24*30), time.Now()); uptime != 0.5 { + t.Errorf("the uptime over the past 30d should've been 0.5, got %f", uptime) + } ssFromOldStore, _ := store.GetEndpointStatus(testEndpoint.Group, testEndpoint.Name, paging.NewEndpointStatusParams().WithResults(1, common.MaximumNumberOfResults).WithEvents(1, common.MaximumNumberOfEvents)) if ssFromOldStore == nil || ssFromOldStore.Group != "group" || ssFromOldStore.Name != "name" || len(ssFromOldStore.Events) != 3 || len(ssFromOldStore.Results) != 2 { store.Close()