@@ -14,12 +14,32 @@ import (
1414 "github.com/samber/lo"
1515)
1616
17- var (
17+ const (
1818 // Maximum number of past checks in the in-memory cache
1919 DefaultCacheCount = 5
2020
2121 // Default search window
2222 DefaultCheckQueryWindow = "1h"
23+
24+ // The number of data points that should be strived for
25+ // when aggregating check statuses.
26+ desiredNumOfCheckStatuses = 100
27+ )
28+
29+ var (
30+ // allowed list of window durations that are used when aggregating check statuses.
31+ allowedWindows = []time.Duration {
32+ time .Minute , // 1m
33+ time .Minute * 5 , // 5m
34+ time .Minute * 15 , // 15m
35+ time .Minute * 30 , // 30m
36+ time .Hour , // 1h
37+ time .Hour * 3 , // 3h
38+ time .Hour * 6 , // 6h
39+ time .Hour * 12 , // 12h
40+ time .Hour * 24 , // 24h
41+ time .Hour * 24 * 7 , // 1w
42+ }
2343)
2444
2545type Timeseries struct {
@@ -138,10 +158,84 @@ func (q CheckQueryParams) GetWhereClause() (string, map[string]interface{}, erro
138158 return strings .TrimSpace (clause ), args , nil
139159}
140160
141- func (q CheckQueryParams ) ExecuteDetails (ctx context.Context ) ([]Timeseries , types.Uptime , types.Latency , error ) {
161+ func getBestPartitioner (totalChecks int , rangeDuration time.Duration ) time.Duration {
162+ if totalChecks <= desiredNumOfCheckStatuses {
163+ return 0 // No need to perform window aggregation
164+ }
165+
166+ bestDelta := 100000000 // sufficiently large delta to begin with
167+ bestWindow := allowedWindows [0 ]
168+
169+ for _ , wp := range allowedWindows {
170+ numWindows := int (rangeDuration / wp )
171+ delta := abs (desiredNumOfCheckStatuses - numWindows )
172+
173+ if delta < bestDelta {
174+ bestDelta = delta
175+ bestWindow = wp
176+ } else {
177+ // as soon as we notice that the delta gets worse, we break the loop
178+ break
179+ }
180+ }
181+
182+ numWindows := int (rangeDuration / bestWindow )
183+ if abs (desiredNumOfCheckStatuses - totalChecks ) <= abs (desiredNumOfCheckStatuses - numWindows ) {
184+ // If this best partition creates windows such that the resulting number of data points deviate more
185+ // from the desired data points than the actual data points, then we do not aggregate.
186+ // Example: if there are 144 checks for the duration of 6 days,
187+ // then the best partition, 1 hour, would generate 144 data points.
188+ // But the original data points (120) are closer to 100, so we do not aggregate.
189+ return 0
190+ }
191+
192+ return bestWindow
193+ }
194+
195+ func optimalWindow (ctx context.Context , from , to time.Time ) (time.Duration , error ) {
196+ var view string
197+ timeRange := to .Sub (from )
198+ if timeRange > time .Hour * 24 * 21 {
199+ view = "check_statuses_1d"
200+ } else if timeRange > time .Hour * 48 {
201+ view = "check_statuses_1h"
202+ } else {
203+ return - 1 , nil //
204+ }
205+
206+ q := fmt .Sprintf (`
207+ SELECT
208+ SUM(total) AS total,
209+ MAX(created_at) AS latest,
210+ MIN(created_at) AS earliest
211+ FROM
212+ %s
213+ WHERE
214+ created_at >= ? AND created_at <= ?;` , view )
215+ var total * int
216+ var latest , earliest * time.Time
217+ if err := ctx .DB ().Raw (q , from , to ).Row ().Scan (& total , & latest , & earliest ); err != nil {
218+ return 0 , err
219+ }
220+ if total == nil {
221+ return - 1 , nil //
222+ }
223+
224+ return getBestPartitioner (* total , earliest .Sub (* latest )), nil
225+ }
226+
227+ func CheckStatuses (ctx context.Context , q CheckQueryParams ) ([]Timeseries , types.Uptime , types.Latency , error ) {
142228 start := q .GetStartTime ().Format (time .RFC3339 )
143229 end := q .GetEndTime ().Format (time .RFC3339 )
144230
231+ // For the given ranges try to find the best window using check statuses summary
232+ window , err := optimalWindow (ctx , * q .GetStartTime (), * q .GetEndTime ())
233+ if err != nil {
234+ return nil , types.Uptime {}, types.Latency {}, err
235+ } else if window >= 0 {
236+ q .WindowDuration = window
237+ }
238+
145239 query := `
146240With grouped_by_window AS (
147241 SELECT
@@ -309,3 +403,14 @@ func parseDuration(d string, name string) (clause string, arg interface{}, err e
309403 }
310404 return "" , nil , fmt .Errorf ("start time must be a duration or RFC3339 timestamp" )
311405}
406+
407+ // abs returns the absolute value of i.
408+ // math.Abs only supports float64 and this avoids the needless type conversions
409+ // and ugly expression.
410+ func abs (n int ) int {
411+ if n > 0 {
412+ return n
413+ }
414+
415+ return - n
416+ }
0 commit comments