diff --git a/config/application.rb b/config/application.rb index b3a084e..e36274d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -20,6 +20,12 @@ class Application < Rails::Application config.action_mailer.default_options = { from: 'fake@berkeley.edu' } config.lit_gtag_id = ENV.fetch('LIT_GTAG_ID', nil) + config.x.servers ||= {} + + config.x.servers[:geoserver] = ENV.fetch('GEODATA_GEOSERVER_PUBLIC_HEALTHCHECK_URL', nil) + config.x.servers[:geoserver_secure] = ENV.fetch('GEODATA_GEOSERVER_SECURE_HEALTHCHECK_URL', nil) + config.x.servers[:spatial_server] = ENV.fetch('GEODATA_SPATIAL_HEALTHCHECK_URL', nil) + # The Base URL for the generated sitemap config.x.sitemap.base_url = ENV.fetch('GEODATA_BASE_URL', 'http://localhost:3000') # Silenced by default to minimize log noise diff --git a/config/initializers/okcomputer.rb b/config/initializers/okcomputer.rb index 2a41f10..b4a35b3 100644 --- a/config/initializers/okcomputer.rb +++ b/config/initializers/okcomputer.rb @@ -1,6 +1,8 @@ # initializers/okcomputer.rb # Health checks configuration +require_relative '../../lib/http_head_check' + OkComputer.logger = Rails.logger OkComputer.check_in_parallel = true @@ -11,3 +13,15 @@ # Requires the ping handler on the solr core (/admin/ping). core_baseurl = Blacklight.default_index.connection.uri.to_s.chomp('/') OkComputer::Registry.register 'solr', OkComputer::SolrCheck.new(core_baseurl) + +# Perform a Head request to check geoserver endpoint +geoserver_url = Rails.configuration.x.servers[:geoserver] +OkComputer::Registry.register 'geoserver', GeoDataHealthCheck::HttpHeadCheck.new(geoserver_url) + +# Perform a Head request to check secure_geoserver endpoint +geoserver_secure_url = Rails.configuration.x.servers[:geoserver_secure] +OkComputer::Registry.register 'geoserver_secure', GeoDataHealthCheck::HttpHeadCheck.new(geoserver_secure_url) + +# Perform a Head request to check spatial server endpoint +spatial_server_url = Rails.configuration.x.servers[:spatial_server] +OkComputer::Registry.register 'spatial_server', GeoDataHealthCheck::HttpHeadCheck.new(spatial_server_url) diff --git a/lib/http_head_check.rb b/lib/http_head_check.rb new file mode 100644 index 0000000..1f94efe --- /dev/null +++ b/lib/http_head_check.rb @@ -0,0 +1,54 @@ +module GeoDataHealthCheck + class HttpHeadCheck < OkComputer::Check + ConnectionFailed = Class.new(StandardError) + attr_accessor :url, :request_timeout + + # rubocop:disable Lint/MissingSuper + def initialize(url, request_timeout = 5) + self.url = url + self.request_timeout = request_timeout + end + # rubocop:enable Lint/MissingSuper + + def check + response = perform_request + + if response.is_a?(Net::HTTPOK) || response.is_a?(Net::HTTPRedirection) + mark_message 'Http head check successful.' + else + mark_message "Error: '#{url}' http head check responded, but returned unexpeced HTTP status: #{response.code} #{response.class}. Expected 200 Net::HTTPOK." + mark_failure + end + rescue StandardError => e + mark_message "Error: '#{e.message}'" + mark_failure + end + + def perform_request + head_request + rescue Net::OpenTimeout, Net::ReadTimeout => e + raise ConnectionFailed, "#{url} did not respond within #{request_timeout} seconds: #{e.message}" + rescue ArgumentError => e + raise ConnectionFailed, "Invalid URL format for '#{url}': #{e.class}: #{e.message}" + rescue StandardError => e + raise ConnectionFailed, e.message + end + + private + + def head_request + uri = URI(url) + Net::HTTP.start( + uri.host, + uri.port, + use_ssl: uri.scheme == 'https', + verify_mode: OpenSSL::SSL::VERIFY_PEER, + open_timeout: request_timeout, + read_timeout: request_timeout + ) do |http| + http.head(uri.request_uri) + end + end + + end +end diff --git a/spec/lib/http_head_check_spec.rb b/spec/lib/http_head_check_spec.rb new file mode 100644 index 0000000..595a863 --- /dev/null +++ b/spec/lib/http_head_check_spec.rb @@ -0,0 +1,89 @@ +require 'rails_helper' +require_relative '../../lib/http_head_check' + +RSpec.describe GeoDataHealthCheck::HttpHeadCheck do + let(:url) { 'https://example.com/endpoint' } + subject(:check) { described_class.new(url) } + + describe 'initialization' do + it 'sets the URL' do + expect(check.url).to eq url + end + + it 'sets default timeout to 5 seconds' do + expect(check.request_timeout).to eq 5 + end + + it 'allows custom timeout' do + check = described_class.new(url, 10) + expect(check.request_timeout).to eq 10 + end + end + + describe '#check' do + context 'when request returns 200 OK' do + it 'marks check as successful' do + response = Net::HTTPOK.new('1.1', 200, 'OK') + allow(check).to receive(:perform_request).and_return(response) + + check.check + + expect(check.message).to include('Http head check successful.') + end + end + + context 'when request returns 500 error' do + it 'marks check as failed' do + response = Net::HTTPInternalServerError.new('1.1', 500, 'Error') + allow(check).to receive(:perform_request).and_return(response) + + check.check + + expect(check.message).to include('Error') + end + end + + end + + describe '#perform_request' do + it 'Net::OpenTimeout with ConnectionFailed and formated message' do + check_with_timeout = described_class.new(url, 7) + allow(check_with_timeout).to receive(:head_request).and_raise(Net::OpenTimeout.new('open timeout')) + + expect { check_with_timeout.perform_request }.to raise_error( + GeoDataHealthCheck::HttpHeadCheck::ConnectionFailed, + a_string_including('did not respond within 7 seconds: open timeout') + ) + end + + it 'Net::ReadTimeout with ConnectionFailed and formated message' do + check_with_timeout = described_class.new(url, 9) + allow(check_with_timeout).to receive(:head_request).and_raise(Net::ReadTimeout.new('read timeout')) + + expect { check_with_timeout.perform_request }.to raise_error( + GeoDataHealthCheck::HttpHeadCheck::ConnectionFailed, + a_string_including('did not respond within 9 seconds: Net::ReadTimeout') + ) + end + + it 'StandardError and passes through the message' do + err_msg = 'generic failure' + allow(check).to receive(:head_request).and_raise(StandardError, err_msg) + + expect { check.perform_request }.to raise_error( + GeoDataHealthCheck::HttpHeadCheck::ConnectionFailed, + err_msg + ) + end + + it 'wraps ArgumentError with ConnectionFailed and includes URL and error class' do + bad_check = described_class.new(url) + allow(bad_check).to receive(:head_request).and_raise(ArgumentError, 'invalid URI') + + expect { bad_check.perform_request }.to raise_error( + GeoDataHealthCheck::HttpHeadCheck::ConnectionFailed, + a_string_including("Invalid URL format for '#{url}'", 'ArgumentError', 'invalid URI') + ) + end + end +end diff --git a/spec/requests/okcomputer_spec.rb b/spec/requests/okcomputer_spec.rb index a04d1c8..7801eca 100644 --- a/spec/requests/okcomputer_spec.rb +++ b/spec/requests/okcomputer_spec.rb @@ -8,12 +8,15 @@ it 'returns all checks at /health' do get '/health' - expect(response).to have_http_status :ok + expect(response).to have_http_status :internal_server_error expect(response.parsed_body.keys).to match_array %w[ default database database-migrations solr + geoserver + geoserver_secure + spatial_server ] end end