Skip to content

Commit

Permalink
Lantern Doctor Page (#29)
Browse files Browse the repository at this point in the history
* add separate page model for lantern doctor

* add dashboard view and routes for web to manage lantern incidents
  • Loading branch information
var77 authored May 10, 2024
1 parent 1506a42 commit c2a59e3
Show file tree
Hide file tree
Showing 22 changed files with 707 additions and 66 deletions.
20 changes: 20 additions & 0 deletions migrate/20240509_lantern_doctor_page.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

Sequel.migration do
up do
create_enum(:doctor_page_status, %w[new triggered acknowledged resolved])
create_table(:lantern_doctor_page) do
column :id, :uuid, primary_key: true, default: nil
foreign_key :query_id, :lantern_doctor_query, type: :uuid
foreign_key :page_id, :page, type: :uuid
column :status, :doctor_page_status, null: false, default: "new"
column :db, :text, null: true, default: "postgres"
column :created_at, :timestamptz, null: false, default: Sequel.lit("now()")
column :updated_at, :timestamptz, null: false, default: Sequel.lit("now()")
end
end

down do
drop_table(:lantern_doctor_page)
end
end
44 changes: 44 additions & 0 deletions model/lantern/lantern_doctor_page.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

require_relative "../../model"

class LanternDoctorPage < Sequel::Model
one_to_one :page, class: Page, key: :id, primary_key: :page_id
many_to_one :query, class: LanternDoctorQuery, key: :query_id, primary_key: :id

include ResourceMethods

def self.create_incident(query, db_name, err: "", output: "")
pg = Prog::PageNexus.assemble_with_logs("Healthcheck: #{query.name} failed on #{query.doctor.resource.name} - #{query.doctor.resource.label} (#{db_name})", [query.ubid, query.doctor.ubid], {"stderr" => err, "stdout" => output}, query.severity, "LanternDoctorQueryFailed", query.id, db_name)
LanternDoctorPage.create_with_id(
query_id: query.id,
page_id: pg.id,
status: "new"
)
end

def path
"#{query.doctor.resource.path}/doctor/incidents/#{id}"
end

def error
page.details["logs"]["stderr"]
end

def output
page.details["logs"]["stdout"]
end

def ack
update(status: "acknowledged")
end

def trigger
update(status: "triggered")
end

def resolve
page.incr_resolve
update(status: "resolved")
end
end
17 changes: 8 additions & 9 deletions model/lantern/lantern_doctor_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ class LanternDoctorQuery < Sequel::Model
many_to_one :doctor, class: LanternDoctor, key: :doctor_id, primary_key: :id
many_to_one :parent, class: self, key: :parent_id
one_to_many :children, key: :parent_id, class: self
one_to_many :pages, key: :query_id, primary_key: :id, class: LanternDoctorPage

plugin :association_dependencies, children: :destroy
plugin :association_dependencies, children: :destroy, pages: :destroy
dataset_module Pagination

include ResourceMethods
Expand Down Expand Up @@ -60,13 +61,11 @@ def user
end

def active_pages
tag = Page.generate_tag("LanternDoctorQueryFailed", id)
Page.active.where(Sequel.like(:tag, "%#{tag}-%")).all
LanternDoctorPage.where(query_id: id, status: ["triggered", "acknowledged"]).all
end

def all_pages
tag = Page.generate_tag("LanternDoctorQueryFailed", id)
Page.where(Sequel.like(:tag, "%#{tag}-%")).all
def new_and_active_pages
LanternDoctorPage.where(query_id: id, status: ["new", "triggered", "acknowledged"]).all
end

def run
Expand Down Expand Up @@ -115,12 +114,12 @@ def run
err_msg = e.message
end

pg = Page.from_tag_parts("LanternDoctorQueryFailed", id, db)
pg = LanternDoctorPage.where(query_id: id, db: db).where(Sequel.lit("status != 'resolved' ")).first

if failed && !pg
Prog::PageNexus.assemble_with_logs("Healthcheck: #{name} failed on #{doctor.resource.name} (#{db})", [ubid, doctor.ubid, lantern_server.ubid], {"stderr" => err_msg, "stdout" => output}, severity, "LanternDoctorQueryFailed", id, db)
LanternDoctorPage.create_incident(self, db, err: err_msg, output: output)
elsif !failed && pg
pg.incr_resolve
pg.resolve
end
end

Expand Down
4 changes: 4 additions & 0 deletions model/lantern/lantern_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ def path
"/location/#{location}/lantern/#{name}"
end

def label
(!super.nil? && !super.empty?) ? super : "no-label"
end

def display_state
super || representative_server&.display_state || "unavailable"
end
Expand Down
2 changes: 1 addition & 1 deletion prog/lantern/lantern_doctor_nexus.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def before_run
decr_destroy

lantern_doctor.failed_queries.each {
_1.active_pages.each { |pg| pg.incr_resolve }
_1.new_and_active_pages.each { |pg| pg.resolve }
}

lantern_doctor.destroy
Expand Down
52 changes: 41 additions & 11 deletions routes/api/project/location/lantern/doctor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,48 @@ class CloverApi
}
end

r.get "incidents" do
result = LanternDoctorQuery.where(doctor_id: @lantern_doctor.id, condition: "failed").paginated_result(
cursor: r.params["cursor"],
page_size: r.params["page_size"],
order_column: r.params["order_column"]
)
r.on "incidents" do
r.on String do |incident_id|
incident = LanternDoctorPage[incident_id]

{
items: serialize(result[:records], :detailed),
next_cursor: result[:next_cursor],
count: result[:count]
}
unless incident
response.status = 404
r.halt
end

r.post "trigger" do
incident.trigger
response.status = 204
r.halt
end

r.post "ack" do
incident.ack
response.status = 204
r.halt
end

r.post "resolve" do
incident.resolve
incident.query.update(condition: "healthy")
response.status = 204
r.halt
end
end

r.get true do
result = LanternDoctorQuery.where(doctor_id: @lantern_doctor.id, condition: "failed").paginated_result(
cursor: r.params["cursor"],
page_size: r.params["page_size"],
order_column: r.params["order_column"]
)

{
items: serialize(result[:records], :detailed),
next_cursor: result[:next_cursor],
count: result[:count]
}
end
end

r.on String do |query_id|
Expand Down
12 changes: 12 additions & 0 deletions routes/web/project/lantern-doctor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

class CloverWeb
hash_branch(:project_prefix, "lantern-doctor") do |r|
@serializer = Serializers::Web::LanternDoctorQuery

r.get true do
@lantern_incidents = serialize(LanternDoctorQuery.where(condition: "failed").all, :detailed)
view "lantern-doctor/index"
end
end
end
1 change: 1 addition & 0 deletions routes/web/project/location/lantern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class CloverWeb
r.halt
end
@pg = serialize(pg, :detailed)
r.hash_branches(:project_location_lantern_prefix)

r.get true do
Authorization.authorize(@current_user.id, "Postgres:view", pg.id)
Expand Down
35 changes: 35 additions & 0 deletions routes/web/project/location/lantern/doctor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

class CloverWeb
hash_branch(:project_location_lantern_prefix, "doctor") do |r|
@serializer = Serializers::Api::LanternDoctorQuery
@lantern_doctor = LanternResource[@pg[:id]].doctor

r.on "incidents" do
r.on String do |incident_id|
incident = LanternDoctorPage[incident_id]

unless incident
response.status = 404
r.halt
end

r.post "trigger" do
incident.trigger
r.redirect "#{@project.path}/lantern-doctor"
end

r.post "ack" do
incident.ack
r.redirect "#{@project.path}/lantern-doctor"
end

r.post "resolve" do
incident.resolve
incident.query.update(condition: "healthy")
r.redirect "#{@project.path}/lantern-doctor"
end
end
end
end
end
2 changes: 1 addition & 1 deletion serializers/api/lantern_doctor_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def self.base(query)
structure(:detailed) do |query|
base(query).merge({
incidents: query.active_pages.map {
{summary: _1.summary, error: _1.details["logs"]["stderr"], output: _1.details["logs"]["stdout"]}
{id: _1.id, status: _1.status, summary: _1.page.summary, error: _1.error, output: _1.output, triggered_at: _1.created_at}
}
})
end
Expand Down
30 changes: 30 additions & 0 deletions serializers/web/lantern_doctor_query.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

class Serializers::Web::LanternDoctorQuery < Serializers::Base
def self.base(query)
{
id: query.id,
name: query.name,
label: "#{query.name} - #{query.doctor.resource.name} (#{query.doctor.resource.label})",
type: query.type,
condition: query.condition,
last_checked: query.last_checked,
schedule: query.schedule,
db_name: query.db_name,
sql: query.sql,
severity: query.severity
}
end

structure(:default) do |query|
base(query)
end

structure(:detailed) do |query|
base(query).merge({
incidents: query.new_and_active_pages.map {
{path: _1.path, id: _1.id, status: _1.status, summary: _1.page.summary, error: _1.error, output: _1.output, triggered_at: _1.created_at}
}
})
end
end
67 changes: 67 additions & 0 deletions spec/model/lantern/lantern_doctor_page_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# frozen_string_literal: true

require_relative "../spec_helper"

RSpec.describe LanternDoctorPage do
subject(:lantern_doctor_page) {
described_class.new do |r|
r.id = "6181ddb3-0002-8ad0-9aeb-084832c9273b"
end
}

let(:pg) {
instance_double(
Page,
severity: "critical"
)
}

describe "#create_incident" do
it "creates page" do
query = instance_double(LanternDoctorQuery, ubid: "test", id: "test-id", severity: "error", name: "test", doctor: instance_double(LanternDoctor, ubid: "test-doc-ubid", resource: instance_double(LanternResource, name: "test-res", label: "test-label")))
db_name = "postgres"
err = "test-err"
output = "test-output"
expect(Prog::PageNexus).to receive(:assemble_with_logs).with("Healthcheck: #{query.name} failed on #{query.doctor.resource.name} - #{query.doctor.resource.label} (#{db_name})", [query.ubid, query.doctor.ubid], {"stderr" => err, "stdout" => output}, query.severity, "LanternDoctorQueryFailed", query.id, db_name).and_return(instance_double(Page, id: "test-pg-id"))
doctor_page = instance_double(described_class)
expect(described_class).to receive(:create_with_id).with(query_id: query.id, page_id: "test-pg-id", status: "new").and_return(doctor_page)
expect(described_class.create_incident(query, db_name, err: err, output: output)).to be(doctor_page)
end
end

describe "#properties (logs)" do
it "returns sterr and stdout from logs" do
expect(lantern_doctor_page).to receive(:page).and_return(pg).at_least(:once)
expect(pg).to receive(:details).and_return({"logs" => {"stdout" => "out", "stderr" => "err"}}).at_least(:once)
expect(lantern_doctor_page.error).to eq("err")
expect(lantern_doctor_page.output).to eq("out")
end
end

describe "#actions (trigger, ack, resolve)" do
it "triggers page" do
expect(lantern_doctor_page).to receive(:update).with(status: "triggered")
expect { lantern_doctor_page.trigger }.not_to raise_error
end

it "acks page" do
expect(lantern_doctor_page).to receive(:update).with(status: "acknowledged")
expect { lantern_doctor_page.ack }.not_to raise_error
end

it "resolves page" do
expect(lantern_doctor_page).to receive(:update).with(status: "resolved")
expect(lantern_doctor_page).to receive(:page).and_return(pg).at_least(:once)
expect(pg).to receive(:incr_resolve)
expect { lantern_doctor_page.resolve }.not_to raise_error
end
end

describe "#path" do
it "returns correct path" do
query = instance_double(LanternDoctorQuery, ubid: "test", id: "test-id", severity: "error", name: "test", doctor: instance_double(LanternDoctor, ubid: "test-doc-ubid", resource: instance_double(LanternResource, name: "test-res", label: "test-label", path: "test-path")))
expect(lantern_doctor_page).to receive(:query).and_return(query)
expect(lantern_doctor_page.path).to eq("test-path/doctor/incidents/#{lantern_doctor_page.id}")
end
end
end
Loading

0 comments on commit c2a59e3

Please sign in to comment.