diff --git a/check_summary.go b/check_summary.go index f5e97b0f..5456f1be 100644 --- a/check_summary.go +++ b/check_summary.go @@ -8,9 +8,24 @@ import ( "github.com/flanksource/duty/models" "github.com/jackc/pgx/v5/pgxpool" + "golang.org/x/exp/slices" "gorm.io/gorm" ) +type CheckSummarySortBy string + +var CheckSummarySortByName CheckSummarySortBy = "name" + +type CheckSummaryOptions struct { + SortBy CheckSummarySortBy +} + +func OrderByName() CheckSummaryOptions { + return CheckSummaryOptions{ + SortBy: CheckSummarySortByName, + } +} + func CheckSummary(ctx DBContext, checkID string) (*models.CheckSummary, error) { var checkSummary models.CheckSummary if err := ctx.DB().First(&checkSummary, "id = ?", checkID).Error; err != nil { @@ -24,7 +39,7 @@ func CheckSummary(ctx DBContext, checkID string) (*models.CheckSummary, error) { return &checkSummary, nil } -func QueryCheckSummary(ctx context.Context, dbpool *pgxpool.Pool) (models.Checks, error) { +func QueryCheckSummary(ctx context.Context, dbpool *pgxpool.Pool, opts ...CheckSummaryOptions) (models.Checks, error) { if _, ok := ctx.Deadline(); !ok { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, DefaultQueryTimeout) @@ -51,6 +66,25 @@ func QueryCheckSummary(ctx context.Context, dbpool *pgxpool.Pool) (models.Checks results = append(results, checks...) } + if len(opts) > 0 && opts[0].SortBy != "" { + slice := []*models.Check(results) + slices.SortFunc(slice, func(a, b *models.Check) int { + var _a, _b string + if opts[0].SortBy == CheckSummarySortByName { + _a = a.Name + _b = b.Name + } + if _a > _b { + return 1 + } + if _a == _b { + return 0 + } + return -1 + }) + return models.Checks(slice), nil + } + return results, nil } diff --git a/check_summary_test.go b/check_summary_test.go index 623ae69a..8b1a929b 100644 --- a/check_summary_test.go +++ b/check_summary_test.go @@ -2,7 +2,6 @@ package duty import ( "context" - "encoding/json" "github.com/flanksource/duty/testutils" ginkgo "github.com/onsi/ginkgo/v2" @@ -10,15 +9,10 @@ import ( ) func testCheckSummaryJSON(path string) { - result, err := QueryCheckSummary(context.Background(), testutils.TestDBPGPool) + result, err := QueryCheckSummary(context.Background(), testutils.TestDBPGPool, OrderByName()) Expect(err).ToNot(HaveOccurred()) - resultJSON, err := json.Marshal(result) - Expect(err).ToNot(HaveOccurred()) - - expected := readTestFile(path) - jqExpr := `del(.[].uptime.last_pass) | del(.[].uptime.last_fail) | del(.[].created_at) | del(.[].updated_at) | del(.[].agent_id)` - matchJSON([]byte(expected), resultJSON, &jqExpr) + match(path, result, `del(.[].uptime.last_pass) | del(.[].uptime.last_fail) | del(.[].created_at) | del(.[].updated_at) | del(.[].agent_id)`) } var _ = ginkgo.Describe("Check summary behavior", ginkgo.Ordered, func() { diff --git a/fixtures/dummy/check_statuses.go b/fixtures/dummy/check_statuses.go index 0096bede..3b59062e 100644 --- a/fixtures/dummy/check_statuses.go +++ b/fixtures/dummy/check_statuses.go @@ -7,31 +7,25 @@ import ( ) var t1 = currentTime.Add(-15 * time.Minute) -var t2 = currentTime.Add(-10 * time.Minute) var t3 = currentTime.Add(-5 * time.Minute) -var LogisticsAPIHealthHTTPCheckStatus1 = models.CheckStatus{ - CheckID: LogisticsAPIHealthHTTPCheck.ID, - Duration: 100, - Status: true, - CreatedAt: t1, - Time: t1.Format("2006-01-02 15:04:05"), -} - -var LogisticsAPIHealthHTTPCheckStatus2 = models.CheckStatus{ - CheckID: LogisticsAPIHealthHTTPCheck.ID, - Duration: 100, - Status: true, - CreatedAt: t2, - Time: t2.Format("2006-01-02 15:04:05"), -} - -var LogisticsAPIHealthHTTPCheckStatus3 = models.CheckStatus{ - CheckID: LogisticsAPIHealthHTTPCheck.ID, - Duration: 100, - Status: true, - CreatedAt: t3, - Time: t3.Format("2006-01-02 15:04:05"), +func generateStatus(check models.Check, t time.Time, count int, passingMod int) []models.CheckStatus { + var statuses = []models.CheckStatus{} + + for i := 0; i < count; i++ { + status := true + if i%passingMod == 0 { + status = false + } + statuses = append(statuses, models.CheckStatus{ + CheckID: check.ID, + Status: status, + CreatedAt: t, + Duration: (1 + i) * 20, + Time: t.Add(time.Minute * time.Duration(i)).Format(time.DateTime), + }) + } + return statuses } var LogisticsAPIHomeHTTPCheckStatus1 = models.CheckStatus{ @@ -39,21 +33,18 @@ var LogisticsAPIHomeHTTPCheckStatus1 = models.CheckStatus{ Duration: 100, Status: true, CreatedAt: t1, - Time: t3.Format("2006-01-02 15:04:05"), + Time: t3.Format(time.DateTime), } -var LogisticsDBCheckStatus1 = models.CheckStatus{ +var OlderThan1H = models.CheckStatus{ CheckID: LogisticsDBCheck.ID, Duration: 50, Status: false, CreatedAt: t1, - Time: t1.Format("2006-01-02 15:04:05"), + Time: time.Now().Add(-70 * time.Minute).Format(time.DateTime), } -var AllDummyCheckStatuses = []models.CheckStatus{ - LogisticsAPIHealthHTTPCheckStatus1, - LogisticsAPIHealthHTTPCheckStatus2, - LogisticsAPIHealthHTTPCheckStatus3, +var AllDummyCheckStatuses = append( + generateStatus(LogisticsAPIHealthHTTPCheck, time.Now(), 70, 5), LogisticsAPIHomeHTTPCheckStatus1, - LogisticsDBCheckStatus1, -} + OlderThan1H) diff --git a/fixtures/expectations/check_status_summary.json b/fixtures/expectations/check_status_summary.json index 63021689..2ab45ab0 100644 --- a/fixtures/expectations/check_status_summary.json +++ b/fixtures/expectations/check_status_summary.json @@ -1,58 +1,98 @@ [ - { - "id": "0186b7a4-0593-73e9-7e3d-5b3446336c1d", - "canary_id": "0186b7a5-a2a4-86fd-c326-3a2104a2777f", - "type": "http", - "name": "logistics-api-health-check", - "canary_name": "dummy-logistics-api-canary", - "namespace": "logistics", - "status": "healthy", - "labels": {}, - "uptime": { - "passed": 3, - "failed": 0, - "last_pass": "2023-03-06T21:24:30" - }, - "latency": { - "p99": 100, - "rolling1h": 0 - } - }, - { - "canary_id": "0186b7a5-a2a4-86fd-c326-3a2104a2777f", - "canary_name": "dummy-logistics-api-canary", - "id": "0186b7a4-625a-6a38-a9a7-e5e6b44ffec3", - "labels": {}, - "latency": { - "p99": 100, - "rolling1h": 0 - }, - "name": "logistics-api-home-check", - "namespace": "logistics", - "status": "healthy", - "type": "http", - "uptime": { - "failed": 0, - "passed": 1 - } - }, - { - "id": "0186b7a4-9338-7142-1b10-25dc49030218", - "canary_id": "0186b7a5-f246-3628-0d68-30bffc13244d", - "type": "postgres", - "name": "logistics-db-check", - "canary_name": "dummy-logistics-db-canary", - "status": "unhealthy", - "namespace": "logistics", - "labels": {}, - "uptime": { - "passed": 0, - "failed": 1, - "last_fail": "2023-03-06T21:14:30" - }, - "latency": { - "p99": 50, - "rolling1h": 0 - } - } + { + "agent_id": "00000000-0000-0000-0000-000000000000", + "canary_id": "6dc9d6dd-0b55-4801-837c-352d3abf9b70", + "canary_name": "dummy-cart-api-canary", + "created_at": "2023-11-01T21:47:47.647714+02:00", + "id": "eed7bd6e-529b-4693-aca9-43977bcc5ff1", + "labels": {}, + "latency": { + "rolling1h": 0 + }, + "name": "cart-api-health-check", + "namespace": "cart", + "status": "healthy", + "type": "http", + "updated_at": "2023-11-01T21:47:47.647714+02:00", + "uptime": { + "failed": 0, + "passed": 0 + } + }, + { + "agent_id": "00000000-0000-0000-0000-000000000000", + "canary_id": "0186b7a5-a2a4-86fd-c326-3a2104a2777f", + "canary_name": "dummy-logistics-api-canary", + "created_at": "2023-11-01T21:47:47.646435+02:00", + "id": "0186b7a4-0593-73e9-7e3d-5b3446336c1d", + "labels": {}, + "latency": { + "avg": 710, + "p50": 700, + "p95": 1340, + "p99": 1400, + "rolling1h": 0 + }, + "name": "logistics-api-health-check", + "namespace": "logistics", + "status": "healthy", + "type": "http", + "updated_at": "2023-11-01T21:47:47.646435+02:00", + "uptime": { + "failed": 14, + "last_fail": "2023-11-01T22:52:28+02:00", + "last_pass": "2023-11-01T22:56:28+02:00", + "passed": 56 + } + }, + { + "agent_id": "00000000-0000-0000-0000-000000000000", + "canary_id": "0186b7a5-a2a4-86fd-c326-3a2104a2777f", + "canary_name": "dummy-logistics-api-canary", + "created_at": "2023-11-01T21:47:47.64714+02:00", + "id": "0186b7a4-625a-6a38-a9a7-e5e6b44ffec3", + "labels": {}, + "latency": { + "avg": 100, + "p50": 100, + "p95": 100, + "p99": 100, + "rolling1h": 0 + }, + "name": "logistics-api-home-check", + "namespace": "logistics", + "status": "healthy", + "type": "http", + "updated_at": "2023-11-01T21:47:47.64714+02:00", + "uptime": { + "failed": 0, + "last_pass": "2023-11-01T21:42:28+02:00", + "passed": 1 + } + }, + { + "agent_id": "00000000-0000-0000-0000-000000000000", + "canary_id": "0186b7a5-f246-3628-0d68-30bffc13244d", + "canary_name": "dummy-logistics-db-canary", + "created_at": "2023-11-01T21:47:47.647428+02:00", + "id": "0186b7a4-9338-7142-1b10-25dc49030218", + "labels": {}, + "latency": { + "avg": 50, + "p50": 50, + "p95": 50, + "p99": 50, + "rolling1h": 0 + }, + "name": "logistics-db-check", + "namespace": "logistics", + "status": "unhealthy", + "type": "postgres", + "updated_at": "2023-11-01T21:47:47.647428+02:00", + "uptime": { + "failed": 1, + "last_fail": "2023-11-01T20:37:28+02:00", + "passed": 0 + } + } ] diff --git a/go.mod b/go.mod index fc395db7..13a3c64e 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/flanksource/postq v0.1.1 github.com/google/uuid v1.3.1 github.com/hashicorp/hcl/v2 v2.18.1 + github.com/hexops/gotextdiff v1.0.3 github.com/itchyny/gojq v0.12.13 github.com/jackc/pgx/v5 v5.4.3 github.com/json-iterator/go v1.1.12 @@ -26,6 +27,7 @@ require ( go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0 go.opentelemetry.io/otel/sdk v1.19.0 go.opentelemetry.io/otel/trace v1.19.0 + golang.org/x/exp v0.0.0-20231006140011-7918f672742d gorm.io/driver/postgres v1.5.3 gorm.io/gorm v1.25.5 k8s.io/api v0.28.2 @@ -137,7 +139,6 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/crypto v0.14.0 // indirect - golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/oauth2 v0.13.0 // indirect golang.org/x/sync v0.4.0 // indirect diff --git a/go.sum b/go.sum index d4439cfa..29504fa1 100644 --- a/go.sum +++ b/go.sum @@ -1017,6 +1017,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/henvic/httpretty v0.1.2 h1:EQo556sO0xeXAjP10eB+BZARMuvkdGqtfeS4Ntjvkiw= github.com/henvic/httpretty v0.1.2/go.mod h1:ViEsly7wgdugYtymX54pYp6Vv2wqZmNHayJ6q8tlKCc= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= diff --git a/models/checks.go b/models/checks.go index bf62b7e7..35b6eb29 100644 --- a/models/checks.go +++ b/models/checks.go @@ -102,7 +102,7 @@ type CheckStatus struct { } func (s CheckStatus) GetTime() (time.Time, error) { - return time.Parse("2006-01-02 15:04:05", s.Time) + return time.Parse(time.DateTime, s.Time) } func (CheckStatus) TableName() string { diff --git a/models/playbooks.go b/models/playbooks.go index aec7daf7..201d3526 100644 --- a/models/playbooks.go +++ b/models/playbooks.go @@ -21,14 +21,16 @@ const ( ) type Playbook struct { - ID uuid.UUID `gorm:"default:generate_ulid()" json:"id"` - Name string `json:"name"` - Spec types.JSON `json:"spec"` - Source string `json:"source"` - CreatedBy *uuid.UUID `json:"created_by,omitempty"` - CreatedAt time.Time `json:"created_at,omitempty" time_format:"postgres_timestamp" gorm:"<-:false"` - UpdatedAt time.Time `json:"updated_at,omitempty" time_format:"postgres_timestamp" gorm:"<-:false"` - DeletedAt *time.Time `json:"deleted_at,omitempty" time_format:"postgres_timestamp"` + ID uuid.UUID `gorm:"default:generate_ulid()" json:"id"` + Name string `json:"name"` + Icon string `json:"icon,omitempty"` + Description string `json:"description,omitempty"` + Spec types.JSON `json:"spec"` + Source string `json:"source"` + CreatedBy *uuid.UUID `json:"created_by,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty" time_format:"postgres_timestamp" gorm:"<-:false"` + UpdatedAt time.Time `json:"updated_at,omitempty" time_format:"postgres_timestamp" gorm:"<-:false"` + DeletedAt *time.Time `json:"deleted_at,omitempty" time_format:"postgres_timestamp"` } func (p Playbook) AsMap(removeFields ...string) map[string]any { diff --git a/schema/playbooks.hcl b/schema/playbooks.hcl index 7bc1d235..2ed61e12 100644 --- a/schema/playbooks.hcl +++ b/schema/playbooks.hcl @@ -9,6 +9,14 @@ table "playbooks" { null = false type = text } + column "icon" { + null = true + type = text + } + column "description" { + null = true + type = text + } column "spec" { null = false type = jsonb diff --git a/suite_test.go b/suite_test.go index c3754c01..5e9de044 100644 --- a/suite_test.go +++ b/suite_test.go @@ -11,6 +11,8 @@ import ( "github.com/flanksource/commons/logger" "github.com/flanksource/duty/fixtures/dummy" "github.com/flanksource/duty/testutils" + "github.com/hexops/gotextdiff" + "github.com/hexops/gotextdiff/myers" "github.com/itchyny/gojq" ginkgo "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -98,7 +100,18 @@ func readTestFile(path string) string { } func writeTestResult(path string, data []byte) { - _ = os.WriteFile(path+".out.json", data, 0644) + d, _ := normalizeJSON(string(data)) + _ = os.WriteFile(path+".out.json", []byte(d), 0644) +} + +func match(path string, result any, jqFilter string) { + resultJSON, err := json.Marshal(result) + + Expect(err).ToNot(HaveOccurred()) + + writeTestResult(path, resultJSON) + expected := readTestFile(path) + matchJSON([]byte(expected), resultJSON, &jqFilter) } func parseJQ(v []byte, expr string) ([]byte, error) { @@ -130,6 +143,46 @@ func parseJQ(v []byte, expr string) ([]byte, error) { return jsonVal, nil } +// normalizeJSON returns an indented json string. +// The keys are sorted lexicographically. +func normalizeJSON(jsonStr string) (string, error) { + var jsonStrMap interface{} + if err := json.Unmarshal([]byte(jsonStr), &jsonStrMap); err != nil { + return "", err + } + + jsonStrIndented, err := json.MarshalIndent(jsonStrMap, "", "\t") + if err != nil { + return "", err + } + + return string(jsonStrIndented), nil +} + +// generateDiff calculates the diff (git style) between the given 2 configs. +func generateDiff(newConf, prevConfig string) (string, error) { + // We want a nicely indented json config with each key-vals in new line + // because that gives us a better diff. A one-line json string config produces diff + // that's not very helpful. + before, err := normalizeJSON(prevConfig) + if err != nil { + return "", fmt.Errorf("failed to normalize json for previous config: %w", err) + } + + after, err := normalizeJSON(newConf) + if err != nil { + return "", fmt.Errorf("failed to normalize json for new config: %w", err) + } + + edits := myers.ComputeEdits("", before, after) + if len(edits) == 0 { + return "", nil + } + + diff := fmt.Sprint(gotextdiff.ToUnified("before", "after", before, edits)) + return diff, nil +} + func matchJSON(actual []byte, expected []byte, jqExpr *string) { var valueA, valueB = actual, expected var err error @@ -145,5 +198,8 @@ func matchJSON(actual []byte, expected []byte, jqExpr *string) { } } - Expect(valueA).To(MatchJSON(valueB)) + + diff, err := generateDiff(string(valueA), string(valueB)) + Expect(err).To(BeNil()) + Expect(diff).To(BeEmpty()) } diff --git a/types/latency.go b/types/latency.go index 317a5571..b88059db 100644 --- a/types/latency.go +++ b/types/latency.go @@ -11,8 +11,9 @@ import ( type Latency struct { Percentile99 float64 `json:"p99,omitempty" db:"p99"` - Percentile97 float64 `json:"p97,omitempty" db:"p97"` Percentile95 float64 `json:"p95,omitempty" db:"p95"` + Percentile50 float64 `json:"p50,omitempty" db:"p50"` + Avg float64 `json:"avg,omitempty" db:"mean"` Rolling1H float64 `json:"rolling1h"` } diff --git a/views/013_check_summary.sql b/views/013_check_summary.sql index 2c73fef7..66dd065e 100644 --- a/views/013_check_summary.sql +++ b/views/013_check_summary.sql @@ -1,51 +1,72 @@ --- Materialized view for check status summary -CREATE MATERIALIZED VIEW IF NOT EXISTS - check_status_summary AS -SELECT - check_id, - PERCENTILE_DISC(0.99) WITHIN GROUP ( - ORDER BY - duration - ) as p99, - COUNT(*) FILTER ( - WHERE - status = TRUE - ) as passed, - COUNT(*) FILTER ( - WHERE - status = FALSE - ) as failed, - MAX(time) as last_check, - MAX(time) FILTER ( - WHERE - status = TRUE - ) as last_pass, - MAX(time) FILTER ( - WHERE - status = FALSE - ) as last_fail -FROM - check_statuses -WHERE - time > (NOW() at TIME ZONE 'utc' - Interval '1 hour') -GROUP BY - check_id; +DROP FUNCTION IF EXISTS check_summary_for_component; +DROP VIEW IF EXISTS check_summary; +DROP MATERIALIZED VIEW IF EXISTS check_status_summary; +DROP VIEW IF EXISTS check_status_summary_aged; +DROP VIEW IF EXISTS check_status_summary_hour; --- For last transition -CREATE -OR REPLACE FUNCTION update_last_transition_time_for_check () RETURNS TRIGGER AS $$ -BEGIN - NEW.last_transition_time = NOW(); - RETURN NEW; -END -$$ LANGUAGE plpgsql; +CREATE OR REPLACE VIEW check_status_summary_hour as + SELECT + check_id, + PERCENTILE_DISC(0.99) WITHIN GROUP ( + ORDER BY + duration + ) as p99, + PERCENTILE_DISC(0.95) WITHIN GROUP ( + ORDER BY + duration + ) as p95, + PERCENTILE_DISC(0.50) WITHIN GROUP ( + ORDER BY + duration + ) as p50, + avg(duration) as mean, + COUNT(*) FILTER ( + WHERE + status = TRUE + ) as passed, + COUNT(*) FILTER ( + WHERE + status = FALSE + ) as failed, + MAX(time) as last_check, + MAX(time) FILTER ( + WHERE + status = TRUE + ) as last_pass, + MAX(time) FILTER ( + WHERE + status = FALSE + ) as last_fail + FROM + check_statuses + WHERE + time > (NOW() at TIME ZONE 'utc' - Interval '1 hour') GROUP BY + check_id; + +CREATE OR REPLACE VIEW check_status_summary_aged as + SELECT DISTINCT ON (check_id) check_id, + duration AS p99, + duration as p95, + duration AS p50, + duration AS mean, + CASE WHEN check_statuses.status = TRUE THEN 1 ELSE 0 END AS passed, + CASE WHEN check_statuses.status = FALSE THEN 1 ELSE 0 END AS failed, + time AS last_check, + CASE WHEN check_statuses.status = TRUE THEN TIME ELSE NULL END AS last_pass, + CASE WHEN check_statuses.status = FALSE THEN TIME ELSE NULL END AS last_fail + FROM check_statuses + inner join checks ON check_statuses.check_id = checks.id + WHERE checks.deleted_at IS NULL and check_id not in (select check_id from check_status_summary_hour) + + ORDER BY check_id, + TIME DESC; + +CREATE MATERIALIZED VIEW IF NOT EXISTS check_status_summary AS + SELECT check_id, p99,p95, p50, mean, passed, failed, last_check, last_pass, last_fail from check_status_summary_hour + UNION + SELECT check_id, p99,p95, p50, mean, passed, failed, last_check, last_pass, last_fail from check_status_summary_aged; -CREATE -OR REPLACE TRIGGER checks_last_transition_time BEFORE -UPDATE ON checks FOR EACH ROW WHEN (OLD.status IS DISTINCT FROM NEW.status) -EXECUTE PROCEDURE update_last_transition_time_for_check (); --- check summary view CREATE OR REPLACE VIEW check_summary AS SELECT checks.id, @@ -56,7 +77,7 @@ CREATE OR REPLACE VIEW check_summary AS 'last_pass', check_status_summary.last_pass, 'last_fail', check_status_summary.last_fail ) AS uptime, - json_build_object('p99', check_status_summary.p99) AS latency, + json_build_object('p99', check_status_summary.p99, 'p95', check_status_summary.p95, 'p50', check_status_summary.p50, 'avg', check_status_summary.mean) AS latency, checks.last_transition_time, checks.type, checks.icon, @@ -76,10 +97,20 @@ CREATE OR REPLACE VIEW check_summary AS FROM checks INNER JOIN canaries ON checks.canary_id = canaries.id - INNER JOIN check_status_summary ON checks.id = check_status_summary.check_id; + LEFT JOIN check_status_summary ON checks.id = check_status_summary.check_id; + +-- For last transition +CREATE OR REPLACE FUNCTION update_last_transition_time_for_check () RETURNS TRIGGER AS $$ +BEGIN + NEW.last_transition_time = NOW(); + RETURN NEW; +END +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE TRIGGER checks_last_transition_time BEFORE +UPDATE ON checks FOR EACH ROW WHEN (OLD.status IS DISTINCT FROM NEW.status) +EXECUTE PROCEDURE update_last_transition_time_for_check (); --- Check summary by component -DROP FUNCTION IF EXISTS check_summary_for_component; CREATE OR REPLACE FUNCTION check_summary_for_component(id uuid) RETURNS setof check_summary AS $$ @@ -87,7 +118,7 @@ AS $$ RETURN QUERY SELECT check_summary.* FROM check_summary INNER JOIN check_component_relationships - ON check_component_relationships.check_id = check_summary.id + ON check_component_relationships.check_id = check_summary.id WHERE check_component_relationships.component_id = $1; END; -$$ language plpgsql; \ No newline at end of file +$$ language plpgsql;