-
Notifications
You must be signed in to change notification settings - Fork 0
feat: [PPT-2077] Added alert & dashboard models #296
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
19 changes: 19 additions & 0 deletions
19
migration/db/migrations/20250917000000001_add_alert_dashboard_table.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| -- +micrate Up | ||
| -- SQL in section 'Up' is executed when this migration is applied | ||
|
|
||
| CREATE TABLE IF NOT EXISTS "alert_dashboard"( | ||
| created_at TIMESTAMPTZ NOT NULL, | ||
| updated_at TIMESTAMPTZ NOT NULL, | ||
| name TEXT NOT NULL, | ||
| description TEXT NOT NULL, | ||
| enabled BOOLEAN NOT NULL, | ||
| authority_id TEXT NOT NULL, | ||
| id TEXT NOT NULL PRIMARY KEY, | ||
| FOREIGN KEY (authority_id) REFERENCES authority(id) ON DELETE CASCADE | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS alert_dashboard_authority_id_index ON "alert_dashboard" USING BTREE (authority_id); | ||
|
|
||
| -- +micrate Down | ||
| -- SQL section 'Down' is executed when this migration is rolled back | ||
| DROP TABLE IF EXISTS "alert_dashboard" |
70 changes: 70 additions & 0 deletions
70
migration/db/migrations/20250917000000002_add_alert_table.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| -- +micrate Up | ||
| -- SQL in section 'Up' is executed when this migration is applied | ||
|
|
||
| -- +micrate StatementBegin | ||
| DO | ||
| $$ | ||
| BEGIN | ||
| IF NOT EXISTS (SELECT * | ||
| FROM pg_type typ | ||
| INNER JOIN pg_namespace nsp | ||
| ON nsp.oid = typ.typnamespace | ||
| WHERE nsp.nspname = current_schema() | ||
| AND typ.typname = 'alert_severity') THEN | ||
| CREATE TYPE alert_severity AS ENUM ( | ||
| 'LOW', | ||
| 'MEDIUM', | ||
| 'HIGH', | ||
| 'CRITICAL' | ||
| ); | ||
| END IF; | ||
| END; | ||
| $$ | ||
| LANGUAGE plpgsql; | ||
| -- +micrate StatementEnd | ||
|
|
||
| -- +micrate StatementBegin | ||
| DO | ||
| $$ | ||
| BEGIN | ||
| IF NOT EXISTS (SELECT * | ||
| FROM pg_type typ | ||
| INNER JOIN pg_namespace nsp | ||
| ON nsp.oid = typ.typnamespace | ||
| WHERE nsp.nspname = current_schema() | ||
| AND typ.typname = 'alert_type') THEN | ||
| CREATE TYPE alert_type AS ENUM ( | ||
| 'THRESHOLD', | ||
| 'STATUS', | ||
| 'CUSTOM' | ||
| ); | ||
| END IF; | ||
| END; | ||
| $$ | ||
| LANGUAGE plpgsql; | ||
| -- +micrate StatementEnd | ||
|
|
||
| CREATE TABLE IF NOT EXISTS "alert"( | ||
| created_at TIMESTAMPTZ NOT NULL, | ||
| updated_at TIMESTAMPTZ NOT NULL, | ||
| name TEXT NOT NULL, | ||
| description TEXT NOT NULL, | ||
| enabled BOOLEAN NOT NULL, | ||
| conditions JSONB NOT NULL, | ||
| severity public.alert_severity NOT NULL DEFAULT 'MEDIUM'::public.alert_severity, | ||
| alert_type public.alert_type NOT NULL DEFAULT 'THRESHOLD'::public.alert_type, | ||
| check_interval INTEGER NOT NULL, | ||
| alert_dashboard_id TEXT NOT NULL, | ||
| id TEXT NOT NULL PRIMARY KEY, | ||
| FOREIGN KEY (alert_dashboard_id) REFERENCES alert_dashboard(id) ON DELETE CASCADE | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS alert_alert_dashboard_id_index ON "alert" USING BTREE (alert_dashboard_id); | ||
| CREATE INDEX IF NOT EXISTS alert_enabled_index ON "alert" USING BTREE (enabled); | ||
| CREATE INDEX IF NOT EXISTS alert_severity_index ON "alert" USING BTREE (severity); | ||
|
|
||
| -- +micrate Down | ||
| -- SQL section 'Down' is executed when this migration is rolled back | ||
| DROP TABLE IF EXISTS "alert"; | ||
| DROP TYPE IF EXISTS public.alert_type; | ||
| DROP TYPE IF EXISTS public.alert_severity; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| require "./helper" | ||
|
|
||
| module PlaceOS::Model | ||
| describe AlertDashboard do | ||
| test_round_trip(AlertDashboard) | ||
|
|
||
| it "saves an alert dashboard" do | ||
| authority = Generator.authority.save! | ||
| inst = Generator.alert_dashboard(authority_id: authority.id).save! | ||
| AlertDashboard.find!(inst.id.as(String)).id.should eq inst.id | ||
| end | ||
|
|
||
| it "validates required fields" do | ||
| invalid_model = Generator.alert_dashboard | ||
| invalid_model.name = "" | ||
| invalid_model.authority_id = nil | ||
|
|
||
| invalid_model.valid?.should be_false | ||
| invalid_model.errors.size.should eq 2 | ||
| invalid_model.errors.map(&.field).should contain(:name) | ||
| invalid_model.errors.map(&.field).should contain(:authority_id) | ||
| end | ||
|
|
||
| it "belongs to authority" do | ||
| authority = Generator.authority.save! | ||
| dashboard = Generator.alert_dashboard(authority_id: authority.id).save! | ||
|
|
||
| dashboard.authority.should_not be_nil | ||
| dashboard.authority.try(&.id).should eq authority.id | ||
| end | ||
|
|
||
| it "has many alerts" do | ||
| authority = Generator.authority.save! | ||
| dashboard = Generator.alert_dashboard(authority_id: authority.id).save! | ||
| alert1 = Generator.alert(alert_dashboard_id: dashboard.id).save! | ||
| alert2 = Generator.alert(alert_dashboard_id: dashboard.id).save! | ||
|
|
||
| dashboard.alerts.size.should eq 2 | ||
| dashboard.alerts.map(&.id).should contain(alert1.id) | ||
| dashboard.alerts.map(&.id).should contain(alert2.id) | ||
| end | ||
|
|
||
| it "counts alerts" do | ||
| authority = Generator.authority.save! | ||
| dashboard = Generator.alert_dashboard(authority_id: authority.id).save! | ||
| Generator.alert(alert_dashboard_id: dashboard.id).save! | ||
| Generator.alert(alert_dashboard_id: dashboard.id).save! | ||
|
|
||
| dashboard.alerts.count.should eq 2 | ||
| end | ||
|
|
||
| it "filters active alerts" do | ||
| authority = Generator.authority.save! | ||
| dashboard = Generator.alert_dashboard(authority_id: authority.id).save! | ||
| active_alert = Generator.alert(alert_dashboard_id: dashboard.id, enabled: true).save! | ||
| Generator.alert(alert_dashboard_id: dashboard.id, enabled: false).save! | ||
|
|
||
| active_alerts = dashboard.active_alerts | ||
| active_alerts.size.should eq 1 | ||
| active_alerts.first.id.should eq active_alert.id | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| require "./helper" | ||
|
|
||
| module PlaceOS::Model | ||
| describe Alert do | ||
| test_round_trip(Alert) | ||
|
|
||
| it "saves an alert" do | ||
| authority = Generator.authority.save! | ||
| dashboard = Generator.alert_dashboard(authority_id: authority.id).save! | ||
| inst = Generator.alert(alert_dashboard_id: dashboard.id).save! | ||
| Alert.find!(inst.id.as(String)).id.should eq inst.id | ||
| end | ||
|
|
||
| it "validates required fields" do | ||
| invalid_model = Generator.alert | ||
| invalid_model.name = "" | ||
| invalid_model.alert_dashboard_id = nil | ||
|
|
||
| invalid_model.valid?.should be_false | ||
| invalid_model.errors.size.should eq 2 | ||
| invalid_model.errors.map(&.field).should contain(:name) | ||
| invalid_model.errors.map(&.field).should contain(:alert_dashboard_id) | ||
| end | ||
|
|
||
| it "belongs to alert dashboard" do | ||
| authority = Generator.authority.save! | ||
| dashboard = Generator.alert_dashboard(authority_id: authority.id).save! | ||
| alert = Generator.alert(alert_dashboard_id: dashboard.id).save! | ||
|
|
||
| alert.alert_dashboard.should_not be_nil | ||
| alert.alert_dashboard.try(&.id).should eq dashboard.id | ||
| end | ||
|
|
||
| it "has default values" do | ||
| alert = Generator.alert | ||
| alert.enabled.should be_true | ||
| alert.severity.should eq Alert::Severity::MEDIUM | ||
| alert.alert_type.should eq Alert::AlertType::THRESHOLD | ||
| alert.check_interval.should eq 60000 | ||
| end | ||
|
|
||
| it "validates conditions" do | ||
| authority = Generator.authority.save! | ||
| dashboard = Generator.alert_dashboard(authority_id: authority.id).save! | ||
| model = Generator.alert(alert_dashboard_id: dashboard.id) | ||
|
|
||
| valid = Trigger::Conditions::TimeDependent.new( | ||
| type: Trigger::Conditions::TimeDependent::Type::At, | ||
| time: Time.utc, | ||
| ) | ||
|
|
||
| invalid = Trigger::Conditions::TimeDependent.new( | ||
| cron: "5 * * * *", | ||
| ) | ||
| model.conditions.try &.time_dependents = [valid, invalid] | ||
|
|
||
| model.valid?.should be_false | ||
| model.errors.size.should eq 1 | ||
| model.errors.first.to_s.should end_with "type should not be nil" | ||
| end | ||
|
|
||
| describe "severity helpers" do | ||
| it "identifies critical alerts" do | ||
| alert = Generator.alert | ||
| alert.severity = Alert::Severity::CRITICAL | ||
| alert.critical?.should be_true | ||
|
|
||
| alert.severity = Alert::Severity::HIGH | ||
| alert.critical?.should be_false | ||
| end | ||
|
|
||
| it "identifies high priority alerts" do | ||
| alert = Generator.alert | ||
|
|
||
| alert.severity = Alert::Severity::CRITICAL | ||
| alert.high_priority?.should be_true | ||
|
|
||
| alert.severity = Alert::Severity::HIGH | ||
| alert.high_priority?.should be_true | ||
|
|
||
| alert.severity = Alert::Severity::MEDIUM | ||
| alert.high_priority?.should be_false | ||
|
|
||
| alert.severity = Alert::Severity::LOW | ||
| alert.high_priority?.should be_false | ||
| end | ||
| end | ||
|
|
||
| describe "enum validation" do | ||
| it "works with valid severity values" do | ||
| authority = Generator.authority.save! | ||
| dashboard = Generator.alert_dashboard(authority_id: authority.id).save! | ||
|
|
||
| Alert::Severity.values.each do |severity| | ||
| alert = Generator.alert(alert_dashboard_id: dashboard.id) | ||
| alert.severity = severity | ||
| alert.valid?.should be_true | ||
| end | ||
| end | ||
|
|
||
| it "works with valid alert type values" do | ||
| authority = Generator.authority.save! | ||
| dashboard = Generator.alert_dashboard(authority_id: authority.id).save! | ||
|
|
||
| Alert::AlertType.values.each do |alert_type| | ||
| alert = Generator.alert(alert_dashboard_id: dashboard.id) | ||
| alert.alert_type = alert_type | ||
| alert.valid?.should be_true | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| require "json" | ||
| require "./base/model" | ||
| require "./trigger/conditions" | ||
|
|
||
| module PlaceOS::Model | ||
| class Alert < ModelBase | ||
| include PlaceOS::Model::Timestamps | ||
|
|
||
| table :alert | ||
|
|
||
| enum Severity | ||
| LOW | ||
| MEDIUM | ||
| HIGH | ||
| CRITICAL | ||
| end | ||
|
|
||
| enum AlertType | ||
| THRESHOLD | ||
| STATUS | ||
| CUSTOM | ||
| end | ||
|
|
||
| attribute name : String, es_subfield: "keyword" | ||
| attribute description : String = "" | ||
| attribute enabled : Bool = true | ||
|
|
||
| # Reuse the same conditions structure as Trigger | ||
| attribute conditions : PlaceOS::Model::Trigger::Conditions = -> { PlaceOS::Model::Trigger::Conditions.new }, es_ignore: true | ||
|
|
||
| attribute severity : Severity = Severity::MEDIUM, converter: PlaceOS::Model::PGEnumConverter(PlaceOS::Model::Alert::Severity) | ||
| attribute alert_type : AlertType = AlertType::THRESHOLD, converter: PlaceOS::Model::PGEnumConverter(PlaceOS::Model::Alert::AlertType) | ||
|
|
||
| # In milliseconds - how often to check the condition | ||
| attribute check_interval : Int32 = 60000 # 1 minute default | ||
|
|
||
| # Association | ||
| ############################################################################################### | ||
|
|
||
| belongs_to AlertDashboard, foreign_key: "alert_dashboard_id" | ||
|
|
||
| # Validation | ||
| ############################################################################################### | ||
|
|
||
| validates :name, presence: true | ||
| validates :alert_dashboard_id, presence: true | ||
|
|
||
| # Validation of conditions | ||
| validate ->(this : Alert) do | ||
| if !this.conditions.valid? | ||
| this.conditions.errors.each do |e| | ||
| this.validation_error(:condition, e.to_s) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| # Helpers | ||
| ############################################################################################### | ||
|
|
||
| def critical? | ||
| severity == Severity::CRITICAL | ||
| end | ||
|
|
||
| def high_priority? | ||
| severity.in?([Severity::HIGH, Severity::CRITICAL]) | ||
| end | ||
| end | ||
| end | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need check_interval as MQTT pushes data as it changes so the frontent will be notified of any changes as soon as they occur.
Hence the conditions can be checked each time there is an update.
What might be useful is a delay on notification, like say don't show low priority issues unless they have been active for 1min without resolving automatically, 30 seconds for medium, 15 seconds for high and 2 seconds for critical (as defaults)
The user could configure alternative times - but the defaults might be simpler to be defined on the frontend and we just have the model default to 15 seconds if the frontend doesn't specify
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pushed change