diff --git a/Gemfile b/Gemfile index ae6c9a7e..d97a5190 100644 --- a/Gemfile +++ b/Gemfile @@ -4,6 +4,7 @@ source "https://rubygems.org" gem "autoprefixer-rails" gem "ddtrace", "~> 0.54" +gem "health-monitor-rails" gem "jquery-rails" gem "lograge" gem "logstash-event" @@ -39,6 +40,7 @@ end group :test do gem "axe-core-rspec" + gem "webmock" end gem "blacklight", "7.31.0" diff --git a/Gemfile.lock b/Gemfile.lock index 36e3970b..e921aabf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -148,6 +148,8 @@ GEM deep_merge (~> 1.2, >= 1.2.1) dry-validation (~> 1.0, >= 1.0.0) connection_pool (2.4.1) + crack (0.4.5) + rexml crass (1.0.6) database_cleaner (2.0.1) database_cleaner-active_record (~> 2.0.0) @@ -320,6 +322,8 @@ GEM tilt (>= 1.2) hashdiff (1.0.1) hashie (5.0.0) + health-monitor-rails (11.3.0) + railties (>= 6.1) honeybadger (5.0.0) httpclient (2.8.3) i18n (1.14.1) @@ -642,6 +646,10 @@ GEM nokogiri (~> 1.6) rubyzip (>= 1.3.0) selenium-webdriver (~> 4.0, < 4.11) + webmock (3.19.1) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) webrick (1.7.0) websocket (1.2.10) websocket-driver (0.7.6) @@ -683,6 +691,7 @@ DEPENDENCIES geoblacklight (= 3.7.0) geoblacklight_sidecar_images google-cloud-storage (~> 1.11) + health-monitor-rails honeybadger jquery-rails lograge @@ -718,7 +727,8 @@ DEPENDENCIES vite_rails web-console webdrivers + webmock whenever BUNDLED WITH - 2.3.18 + 2.4.10 diff --git a/app/checks/solr_status.rb b/app/checks/solr_status.rb new file mode 100644 index 00000000..d8001590 --- /dev/null +++ b/app/checks/solr_status.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +class SolrStatus < HealthMonitor::Providers::Base + def check! + uri = Blacklight.default_index.connection.uri + status_uri = URI(uri.to_s.gsub(uri.path, "/solr/admin/cores?action=STATUS")) + req = Net::HTTP::Get.new(status_uri) + req.basic_auth(uri.user, uri.password) if uri.user && uri.password + response = Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) } + json = JSON.parse(response.body) + raise "The solr has an invalid status #{status_uri}" if json["responseHeader"]["status"] != 0 + end +end diff --git a/config/initializers/health_monitor.rb b/config/initializers/health_monitor.rb new file mode 100644 index 00000000..0e990156 --- /dev/null +++ b/config/initializers/health_monitor.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +Rails.application.config.after_initialize do + HealthMonitor.configure do |config| + config.cache + config.redis unless Rails.env.test? + + config.add_custom_provider(SolrStatus) + + # Make this health check available at /health + config.path = :health + + config.error_callback = proc do |e| + Rails.logger.error "Health check failed with: #{e.message}" + Honeybadger.notify(e) + end + end +end diff --git a/config/routes.rb b/config/routes.rb index d0fffff0..d8cc8bcb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true Rails.application.routes.draw do + mount HealthMonitor::Engine, at: "/" concern :range_searchable, BlacklightRangeLimit::Routes::RangeSearchable.new mount Blacklight::Engine => "/" mount BlacklightAdvancedSearch::Engine => "/" diff --git a/spec/requests/health_check_spec.rb b/spec/requests/health_check_spec.rb new file mode 100644 index 00000000..107ca901 --- /dev/null +++ b/spec/requests/health_check_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe "Health Check", type: :request do + describe "GET /health" do + it "has a health check" do + get "/health.json" + expect(response).to be_successful + end + + it "errors when a service is down" do + allow(Blacklight.default_index.connection).to receive(:uri).and_return(URI("http://example.com/bla")) + stub_request(:get, "http://example.com/solr/admin/cores?action=STATUS").to_return( + body: { responseHeader: { status: 500 } }.to_json, headers: { "Content-Type" => "text/json" } + ) + + get "/health.json" + + expect(response).not_to be_successful + expect(response.status).to eq 503 + solr_response = JSON.parse(response.body)["results"].find { |x| x["name"] == "SolrStatus" } + expect(solr_response["message"]).to start_with "The solr has an invalid status" + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 03e3b00f..84098fce 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true ENV["RACK_ENV"] = "test" +require "webmock/rspec" require "simplecov" require "capybara/rspec" require "capybara-screenshot/rspec" @@ -32,3 +33,9 @@ mocks.verify_partial_doubles = true end end + +WebMock.disable_net_connect!(allow_localhost: true, + net_http_connect_on_start: true, + allow: ["chromedriver.storage.googleapis.com", "googlechromelabs.github.io", + "storage.googleapis.com", + "edgedl.me.gvt1.com"])