Commit 467a411e authored by David Wilkins's avatar David Wilkins Committed by Ash McKenzie

Convert RestClient to Gitlab::HTTP for Prometheus Monitor

- Closes #60024

- Change PrometheusClient.new to accept a base url instead of an
  already created RestClient

- Use Gitlab::HTTP in PrometheusClient instead of creating RestClient
  in PrometheusService

- Move http_options from PrometheusService to
  PrometheusClient (follow_redirects: false)

- ensure that base urls don't have the trailing slash

- Created a `PrometheusClient#url` method that might not be strictly
  required

- Change rescued exceptions from RestClient::* to
  HTTParty::ResponseError where possible and StandardError for the
  rest
parent b67ed9c9
...@@ -83,7 +83,7 @@ module Clusters ...@@ -83,7 +83,7 @@ module Clusters
# ensures headers containing auth data are appended to original k8s client options # ensures headers containing auth data are appended to original k8s client options
options = kube_client.rest_client.options.merge(headers: kube_client.headers) options = kube_client.rest_client.options.merge(headers: kube_client.headers)
RestClient::Resource.new(proxy_url, options) Gitlab::PrometheusClient.new(proxy_url, options)
rescue Kubeclient::HttpError rescue Kubeclient::HttpError
# If users have mistakenly set parameters or removed the depended clusters, # If users have mistakenly set parameters or removed the depended clusters,
# `proxy_url` could raise an exception because gitlab can not communicate with the cluster. # `proxy_url` could raise an exception because gitlab can not communicate with the cluster.
......
...@@ -14,10 +14,6 @@ module PrometheusAdapter ...@@ -14,10 +14,6 @@ module PrometheusAdapter
raise NotImplementedError raise NotImplementedError
end end
def prometheus_client_wrapper
Gitlab::PrometheusClient.new(prometheus_client)
end
def can_query? def can_query?
prometheus_client.present? prometheus_client.present?
end end
...@@ -35,7 +31,7 @@ module PrometheusAdapter ...@@ -35,7 +31,7 @@ module PrometheusAdapter
def calculate_reactive_cache(query_class_name, *args) def calculate_reactive_cache(query_class_name, *args)
return unless prometheus_client return unless prometheus_client
data = Object.const_get(query_class_name, false).new(prometheus_client_wrapper).query(*args) data = Object.const_get(query_class_name, false).new(prometheus_client).query(*args)
{ {
success: true, success: true,
data: data, data: data,
......
...@@ -63,15 +63,16 @@ class PrometheusService < MonitoringService ...@@ -63,15 +63,16 @@ class PrometheusService < MonitoringService
# Check we can connect to the Prometheus API # Check we can connect to the Prometheus API
def test(*args) def test(*args)
Gitlab::PrometheusClient.new(prometheus_client).ping prometheus_client.ping
{ success: true, result: 'Checked API endpoint' } { success: true, result: 'Checked API endpoint' }
rescue Gitlab::PrometheusClient::Error => err rescue Gitlab::PrometheusClient::Error => err
{ success: false, result: err } { success: false, result: err }
end end
def prometheus_client def prometheus_client
RestClient::Resource.new(api_url, max_redirects: 0) if should_return_client? return unless should_return_client?
Gitlab::PrometheusClient.new(api_url)
end end
def prometheus_available? def prometheus_available?
...@@ -84,7 +85,7 @@ class PrometheusService < MonitoringService ...@@ -84,7 +85,7 @@ class PrometheusService < MonitoringService
private private
def should_return_client? def should_return_client?
api_url && manual_configuration? && active? && valid? api_url.present? && manual_configuration? && active? && valid?
end end
def synchronize_service_state def synchronize_service_state
......
...@@ -98,7 +98,7 @@ module Prometheus ...@@ -98,7 +98,7 @@ module Prometheus
end end
def prometheus_client_wrapper def prometheus_client_wrapper
prometheus_adapter&.prometheus_client_wrapper prometheus_adapter&.prometheus_client
end end
def can_query? def can_query?
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
module Gitlab module Gitlab
# Helper methods to interact with Prometheus network services & resources # Helper methods to interact with Prometheus network services & resources
class PrometheusClient class PrometheusClient
include Gitlab::Utils::StrongMemoize
Error = Class.new(StandardError) Error = Class.new(StandardError)
QueryError = Class.new(Gitlab::PrometheusClient::Error) QueryError = Class.new(Gitlab::PrometheusClient::Error)
...@@ -14,10 +15,17 @@ module Gitlab ...@@ -14,10 +15,17 @@ module Gitlab
# Minimal value of the `step` parameter for `query_range` in seconds. # Minimal value of the `step` parameter for `query_range` in seconds.
QUERY_RANGE_MIN_STEP = 60 QUERY_RANGE_MIN_STEP = 60
attr_reader :rest_client, :headers # Key translation between RestClient and Gitlab::HTTP (HTTParty)
RESTCLIENT_GITLAB_HTTP_KEYMAP = {
ssl_cert_store: :cert_store
}.freeze
def initialize(rest_client) attr_reader :api_url, :options
@rest_client = rest_client private :api_url, :options
def initialize(api_url, options = {})
@api_url = api_url.chomp('/')
@options = options
end end
def ping def ping
...@@ -27,14 +35,10 @@ module Gitlab ...@@ -27,14 +35,10 @@ module Gitlab
def proxy(type, args) def proxy(type, args)
path = api_path(type) path = api_path(type)
get(path, args) get(path, args)
rescue RestClient::ExceptionWithResponse => ex rescue Gitlab::HTTP::ResponseError => ex
if ex.response raise PrometheusClient::Error, "Network connection error" unless ex.response && ex.response.try(:code)
ex.response
else handle_response(ex.response)
raise PrometheusClient::Error, "Network connection error"
end
rescue RestClient::Exception
raise PrometheusClient::Error, "Network connection error"
end end
def query(query, time: Time.now) def query(query, time: Time.now)
...@@ -78,50 +82,58 @@ module Gitlab ...@@ -78,50 +82,58 @@ module Gitlab
private private
def api_path(type) def api_path(type)
['api', 'v1', type].join('/') [api_url, 'api', 'v1', type].join('/')
end end
def json_api_get(type, args = {}) def json_api_get(type, args = {})
path = api_path(type) path = api_path(type)
response = get(path, args) response = get(path, args)
handle_response(response) handle_response(response)
rescue RestClient::ExceptionWithResponse => ex rescue Gitlab::HTTP::ResponseError => ex
if ex.response raise PrometheusClient::Error, "Network connection error" unless ex.response && ex.response.try(:code)
handle_exception_response(ex.response)
else handle_response(ex.response)
raise PrometheusClient::Error, "Network connection error" end
def gitlab_http_key(key)
RESTCLIENT_GITLAB_HTTP_KEYMAP[key] || key
end
def mapped_options
options.keys.map { |k| [gitlab_http_key(k), options[k]] }.to_h
end
def http_options
strong_memoize(:http_options) do
{ follow_redirects: false }.merge(mapped_options)
end end
rescue RestClient::Exception
raise PrometheusClient::Error, "Network connection error"
end end
def get(path, args) def get(path, args)
rest_client[path].get(params: args) Gitlab::HTTP.get(path, { query: args }.merge(http_options) )
rescue SocketError rescue SocketError
raise PrometheusClient::Error, "Can't connect to #{rest_client.url}" raise PrometheusClient::Error, "Can't connect to #{api_url}"
rescue OpenSSL::SSL::SSLError rescue OpenSSL::SSL::SSLError
raise PrometheusClient::Error, "#{rest_client.url} contains invalid SSL data" raise PrometheusClient::Error, "#{api_url} contains invalid SSL data"
rescue Errno::ECONNREFUSED rescue Errno::ECONNREFUSED
raise PrometheusClient::Error, 'Connection refused' raise PrometheusClient::Error, 'Connection refused'
end end
def handle_response(response) def handle_response(response)
json_data = parse_json(response.body) response_code = response.try(:code)
if response.code == 200 && json_data['status'] == 'success' response_body = response.try(:body)
json_data['data'] || {}
else raise PrometheusClient::Error, "#{response_code} - #{response_body}" unless response_code
raise PrometheusClient::Error, "#{response.code} - #{response.body}"
end json_data = parse_json(response_body) if [200, 400].include?(response_code)
end
def handle_exception_response(response) case response_code
if response.code == 200 && response['status'] == 'success' when 200
response['data'] || {} json_data['data'] if response['status'] == 'success'
elsif response.code == 400 when 400
json_data = parse_json(response.body)
raise PrometheusClient::QueryError, json_data['error'] || 'Bad data received' raise PrometheusClient::QueryError, json_data['error'] || 'Bad data received'
else else
raise PrometheusClient::Error, "#{response.code} - #{response.body}" raise PrometheusClient::Error, "#{response_code} - #{response_body}"
end end
end end
......
...@@ -67,7 +67,7 @@ module Sentry ...@@ -67,7 +67,7 @@ module Sentry
def handle_request_exceptions def handle_request_exceptions
yield yield
rescue HTTParty::Error => e rescue Gitlab::HTTP::Error => e
Gitlab::Sentry.track_acceptable_exception(e) Gitlab::Sentry.track_acceptable_exception(e)
raise_error 'Error when connecting to Sentry' raise_error 'Error when connecting to Sentry'
rescue Net::OpenTimeout rescue Net::OpenTimeout
......
...@@ -61,7 +61,7 @@ describe 'User activates issue tracker', :js do ...@@ -61,7 +61,7 @@ describe 'User activates issue tracker', :js do
context 'when the connection test fails' do context 'when the connection test fails' do
it 'activates the service' do it 'activates the service' do
stub_request(:head, url).to_raise(HTTParty::Error) stub_request(:head, url).to_raise(Gitlab::HTTP::Error)
click_link(tracker) click_link(tracker)
......
...@@ -48,7 +48,7 @@ describe 'User activates issue tracker', :js do ...@@ -48,7 +48,7 @@ describe 'User activates issue tracker', :js do
context 'when the connection test fails' do context 'when the connection test fails' do
it 'activates the service' do it 'activates the service' do
stub_request(:head, url).to_raise(HTTParty::Error) stub_request(:head, url).to_raise(Gitlab::HTTP::Error)
click_link(tracker) click_link(tracker)
fill_form fill_form
......
...@@ -173,7 +173,7 @@ describe Gitlab::BitbucketImport::Importer do ...@@ -173,7 +173,7 @@ describe Gitlab::BitbucketImport::Importer do
context 'when importing a pull request throws an exception' do context 'when importing a pull request throws an exception' do
before do before do
allow(pull_request).to receive(:raw).and_return('hello world') allow(pull_request).to receive(:raw).and_return('hello world')
allow(subject.client).to receive(:pull_request_comments).and_raise(HTTParty::Error) allow(subject.client).to receive(:pull_request_comments).and_raise(Gitlab::HTTP::Error)
end end
it 'logs an error without the backtrace' do it 'logs an error without the backtrace' do
......
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::PrometheusClient do describe Gitlab::PrometheusClient do
include PrometheusHelpers include PrometheusHelpers
subject { described_class.new(RestClient::Resource.new('https://prometheus.example.com')) } subject { described_class.new('https://prometheus.example.com') }
describe '#ping' do describe '#ping' do
it 'issues a "query" request to the API endpoint' do it 'issues a "query" request to the API endpoint' do
...@@ -79,8 +79,16 @@ describe Gitlab::PrometheusClient do ...@@ -79,8 +79,16 @@ describe Gitlab::PrometheusClient do
expect(req_stub).to have_been_requested expect(req_stub).to have_been_requested
end end
it 'raises a Gitlab::PrometheusClient::Error error when a RestClient::Exception is rescued' do it 'raises a Gitlab::PrometheusClient::Error error when a Gitlab::HTTP::ResponseError is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, RestClient::Exception) req_stub = stub_prometheus_request_with_exception(prometheus_url, Gitlab::HTTP::ResponseError)
expect { subject }
.to raise_error(Gitlab::PrometheusClient::Error, "Network connection error")
expect(req_stub).to have_been_requested
end
it 'raises a Gitlab::PrometheusClient::Error error when a Gitlab::HTTP::ResponseError with a code is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, Gitlab::HTTP::ResponseError.new(code: 400))
expect { subject } expect { subject }
.to raise_error(Gitlab::PrometheusClient::Error, "Network connection error") .to raise_error(Gitlab::PrometheusClient::Error, "Network connection error")
...@@ -89,13 +97,13 @@ describe Gitlab::PrometheusClient do ...@@ -89,13 +97,13 @@ describe Gitlab::PrometheusClient do
end end
context 'ping' do context 'ping' do
subject { described_class.new(RestClient::Resource.new(prometheus_url)).ping } subject { described_class.new(prometheus_url).ping }
it_behaves_like 'exceptions are raised' it_behaves_like 'exceptions are raised'
end end
context 'proxy' do context 'proxy' do
subject { described_class.new(RestClient::Resource.new(prometheus_url)).proxy('query', { query: '1' }) } subject { described_class.new(prometheus_url).proxy('query', { query: '1' }) }
it_behaves_like 'exceptions are raised' it_behaves_like 'exceptions are raised'
end end
...@@ -310,15 +318,32 @@ describe Gitlab::PrometheusClient do ...@@ -310,15 +318,32 @@ describe Gitlab::PrometheusClient do
end end
end end
context 'when RestClient::Exception is raised' do context 'when Gitlab::HTTP::ResponseError is raised' do
before do before do
stub_prometheus_request_with_exception(query_url, RestClient::Exception) stub_prometheus_request_with_exception(query_url, response_error)
end
context "without response code" do
let(:response_error) { Gitlab::HTTP::ResponseError }
it 'raises PrometheusClient::Error' do
expect { subject.proxy('query', { query: prometheus_query }) }.to(
raise_error(Gitlab::PrometheusClient::Error, 'Network connection error')
)
end
end end
it 'raises PrometheusClient::Error' do context "with response code" do
expect { subject.proxy('query', { query: prometheus_query }) }.to( let(:response_error) do
raise_error(Gitlab::PrometheusClient::Error, 'Network connection error') response = Net::HTTPResponse.new(1.1, 400, '{}sumpthin')
) allow(response).to receive(:body) { '{}' }
Gitlab::HTTP::ResponseError.new(response)
end
it 'raises Gitlab::PrometheusClient::QueryError' do
expect { subject.proxy('query', { query: prometheus_query }) }.to(
raise_error(Gitlab::PrometheusClient::QueryError, 'Bad data received')
)
end
end end
end end
end end
......
...@@ -63,7 +63,7 @@ describe Sentry::Client do ...@@ -63,7 +63,7 @@ describe Sentry::Client do
shared_examples 'maps exceptions' do shared_examples 'maps exceptions' do
exceptions = { exceptions = {
HTTParty::Error => 'Error when connecting to Sentry', Gitlab::HTTP::Error => 'Error when connecting to Sentry',
Net::OpenTimeout => 'Connection to Sentry timed out', Net::OpenTimeout => 'Connection to Sentry timed out',
SocketError => 'Received SocketError when trying to connect to Sentry', SocketError => 'Received SocketError when trying to connect to Sentry',
OpenSSL::SSL::SSLError => 'Sentry returned invalid SSL data', OpenSSL::SSL::SSLError => 'Sentry returned invalid SSL data',
......
...@@ -86,16 +86,15 @@ describe Clusters::Applications::Prometheus do ...@@ -86,16 +86,15 @@ describe Clusters::Applications::Prometheus do
project: cluster.cluster_project.project) project: cluster.cluster_project.project)
end end
it 'creates proxy prometheus rest client' do it 'creates proxy prometheus_client' do
expect(subject.prometheus_client).to be_instance_of(RestClient::Resource) expect(subject.prometheus_client).to be_instance_of(Gitlab::PrometheusClient)
end end
it 'creates proper url' do it 'copies proxy_url, options and headers from kube client to prometheus_client' do
expect(subject.prometheus_client.url).to eq("#{kubernetes_url}/api/v1/namespaces/gitlab-managed-apps/services/prometheus-prometheus-server:80/proxy") expect(Gitlab::PrometheusClient)
end .to(receive(:new))
.with(a_valid_url, kube_client.rest_client.options.merge(headers: kube_client.headers))
it 'copies options and headers from kube client to proxy client' do subject.prometheus_client
expect(subject.prometheus_client.options).to eq(kube_client.rest_client.options.merge(headers: kube_client.headers))
end end
context 'when cluster is not reachable' do context 'when cluster is not reachable' do
......
...@@ -40,13 +40,13 @@ describe PrometheusAdapter, :use_clean_rails_memory_store_caching do ...@@ -40,13 +40,13 @@ describe PrometheusAdapter, :use_clean_rails_memory_store_caching do
describe 'matched_metrics' do describe 'matched_metrics' do
let(:matched_metrics_query) { Gitlab::Prometheus::Queries::MatchedMetricQuery } let(:matched_metrics_query) { Gitlab::Prometheus::Queries::MatchedMetricQuery }
let(:prometheus_client_wrapper) { double(:prometheus_client_wrapper, label_values: nil) } let(:prometheus_client) { double(:prometheus_client, label_values: nil) }
context 'with valid data' do context 'with valid data' do
subject { service.query(:matched_metrics) } subject { service.query(:matched_metrics) }
before do before do
allow(service).to receive(:prometheus_client_wrapper).and_return(prometheus_client_wrapper) allow(service).to receive(:prometheus_client).and_return(prometheus_client)
synchronous_reactive_cache(service) synchronous_reactive_cache(service)
end end
......
...@@ -105,10 +105,6 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do ...@@ -105,10 +105,6 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do
context 'manual configuration is enabled' do context 'manual configuration is enabled' do
let(:manual_configuration) { true } let(:manual_configuration) { true }
it 'returns rest client from api_url' do
expect(service.prometheus_client.url).to eq(api_url)
end
it 'calls valid?' do it 'calls valid?' do
allow(service).to receive(:valid?).and_call_original allow(service).to receive(:valid?).and_call_original
......
...@@ -33,7 +33,7 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do ...@@ -33,7 +33,7 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do
before do before do
allow(project).to receive(:lfs_enabled?).and_return(true) allow(project).to receive(:lfs_enabled?).and_return(true)
response = instance_double(HTTParty::Response) response = instance_double(Gitlab::HTTP::Response)
allow(response).to receive(:body).and_return(objects_response.to_json) allow(response).to receive(:body).and_return(objects_response.to_json)
allow(response).to receive(:success?).and_return(true) allow(response).to receive(:success?).and_return(true)
allow(Gitlab::HTTP).to receive(:post).and_return(response) allow(Gitlab::HTTP).to receive(:post).and_return(response)
...@@ -95,7 +95,7 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do ...@@ -95,7 +95,7 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do
shared_examples 'JSON parse errors' do |body| shared_examples 'JSON parse errors' do |body|
it 'raises error' do it 'raises error' do
response = instance_double(HTTParty::Response) response = instance_double(Gitlab::HTTP::Response)
allow(response).to receive(:body).and_return(body) allow(response).to receive(:body).and_return(body)
allow(response).to receive(:success?).and_return(true) allow(response).to receive(:success?).and_return(true)
allow(Gitlab::HTTP).to receive(:post).and_return(response) allow(Gitlab::HTTP).to receive(:post).and_return(response)
......
...@@ -131,7 +131,7 @@ describe Prometheus::ProxyService do ...@@ -131,7 +131,7 @@ describe Prometheus::ProxyService do
allow(environment).to receive(:prometheus_adapter) allow(environment).to receive(:prometheus_adapter)
.and_return(prometheus_adapter) .and_return(prometheus_adapter)
allow(prometheus_adapter).to receive(:can_query?).and_return(true) allow(prometheus_adapter).to receive(:can_query?).and_return(true)
allow(prometheus_adapter).to receive(:prometheus_client_wrapper) allow(prometheus_adapter).to receive(:prometheus_client)
.and_return(prometheus_client) .and_return(prometheus_client)
end end
......
...@@ -5,3 +5,7 @@ RSpec::Matchers.define :be_url do |_| ...@@ -5,3 +5,7 @@ RSpec::Matchers.define :be_url do |_|
URI.parse(actual) rescue false URI.parse(actual) rescue false
end end
end end
# looks better when used like:
# expect(thing).to receive(:method).with(a_valid_url)
RSpec::Matchers.alias_matcher :a_valid_url, :be_url
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment