@@ -220,6 +220,129 @@ FROM pulse.check_executions
220220 return result , nil
221221}
222222
223+ func (e * ExecutionCheck ) ListExecutionTimeline (
224+ ctx context.Context ,
225+ filter model.CheckExecutionTimelineFilter ,
226+ ) ([]model.CheckExecutionTimelineRecord , error ) {
227+ if err := filter .Validate (); err != nil {
228+ return nil , err
229+ }
230+
231+ bucketStepUs , err := executionBucketStepMicros (filter .Bucket )
232+ if err != nil {
233+ return nil , err
234+ }
235+ staleAfterUs := 2 * filter .Interval .Microseconds ()
236+
237+ query := `
238+ WITH params AS (
239+ SELECT
240+ $1::text AS service_id,
241+ $2::text AS check_id,
242+ $3::timestamptz AS from_ts,
243+ $4::timestamptz AS to_ts,
244+ $5::bigint AS bucket_step_us,
245+ $6::bigint AS stale_after_us
246+ ),
247+ buckets AS (
248+ SELECT
249+ gs AS bucket_start,
250+ gs + (p.bucket_step_us * interval '1 microsecond') AS bucket_end
251+ FROM params p,
252+ LATERAL generate_series(
253+ p.from_ts,
254+ p.to_ts,
255+ p.bucket_step_us * interval '1 microsecond'
256+ ) AS gs
257+ ),
258+ last_execution_per_bucket AS (
259+ SELECT
260+ b.bucket_start,
261+ b.bucket_end,
262+ e.finished_at AS last_observed_at,
263+ e.status AS last_execution_status
264+ FROM buckets b
265+ CROSS JOIN params p
266+ LEFT JOIN LATERAL (
267+ SELECT
268+ ce.finished_at,
269+ ce.status
270+ FROM pulse.check_executions ce
271+ WHERE ce.service_id = p.service_id
272+ AND ce.check_id = p.check_id
273+ AND ce.finished_at <= b.bucket_end
274+ ORDER BY ce.finished_at DESC
275+ LIMIT 1
276+ ) e ON TRUE
277+ )
278+ SELECT
279+ bucket_start,
280+ bucket_end,
281+ last_observed_at,
282+ last_execution_status,
283+ CASE
284+ WHEN last_observed_at IS NULL THEN 'unknown'::pulse.check_state_status
285+ WHEN bucket_end - last_observed_at > (
286+ (SELECT stale_after_us FROM params) * interval '1 microsecond'
287+ ) THEN 'unknown'::pulse.check_state_status
288+ WHEN last_execution_status = 'success' THEN 'healthy'::pulse.check_state_status
289+ WHEN last_execution_status = 'failure' THEN 'unhealthy'::pulse.check_state_status
290+ ELSE 'unknown'::pulse.check_state_status
291+ END AS timeline_state
292+ FROM last_execution_per_bucket
293+ ORDER BY bucket_start
294+ `
295+
296+ rows , err := e .db .Query (
297+ ctx ,
298+ query ,
299+ filter .ServiceID ,
300+ filter .CheckID ,
301+ filter .From ,
302+ filter .To ,
303+ bucketStepUs ,
304+ staleAfterUs ,
305+ )
306+ if err != nil {
307+ return nil , err
308+ }
309+ defer rows .Close ()
310+
311+ var result []model.CheckExecutionTimelineRecord
312+
313+ for rows .Next () {
314+ var (
315+ row model.CheckExecutionTimelineRecord
316+ rawObservedAt * time.Time
317+ rawExecStatus * string
318+ )
319+
320+ err = rows .Scan (
321+ & row .BucketStart ,
322+ & row .BucketEnd ,
323+ & rawObservedAt ,
324+ & rawExecStatus ,
325+ & row .State ,
326+ )
327+ if err != nil {
328+ return nil , err
329+ }
330+
331+ row .LastObservedAt = rawObservedAt
332+ if rawExecStatus != nil {
333+ row .LastExecutionStatus = new (model.CheckExecutionStatus (* rawExecStatus ))
334+ }
335+
336+ result = append (result , row )
337+ }
338+
339+ if err = rows .Err (); err != nil {
340+ return nil , err
341+ }
342+
343+ return result , nil
344+ }
345+
223346func mustField (name string ) string {
224347 switch name {
225348 case "service_id" , "check_id" , "finished_at" :
@@ -244,6 +367,21 @@ func executionBucketExpr(bucket model.CheckExecutionBucket) (string, error) {
244367 }
245368}
246369
370+ func executionBucketStepMicros (bucket model.CheckExecutionBucket ) (int64 , error ) {
371+ switch bucket {
372+ case model .CheckExecutionBucketSecond :
373+ return time .Second .Microseconds (), nil
374+ case "" , model .CheckExecutionBucketMinute :
375+ return time .Minute .Microseconds (), nil
376+ case model .CheckExecutionBucketHour :
377+ return time .Hour .Microseconds (), nil
378+ case model .CheckExecutionBucketDay :
379+ return (24 * time .Hour ).Microseconds (), nil
380+ default :
381+ return 0 , fmt .Errorf ("unsupported execution bucket %q" , bucket )
382+ }
383+ }
384+
247385func NewExecutionRepository (db repository.QueryExecutor ) * ExecutionCheck {
248386 return & ExecutionCheck {db }
249387}
0 commit comments