Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorReuben Pereira <rpereira@gitlab.com>2019-04-05 11:05:54 +0300
committerSean McGivern <sean@gitlab.com>2019-04-05 11:05:54 +0300
commit7f529353e75852e62203eee3b8106b21dca068d9 (patch)
treed009d7fbd7c8cb6740cf6cdae30407f8b8582aa7
parent88bf73a719933c136cc2bd9b791b6d5ca85cd330 (diff)
Add a proxy method to PrometheusClient
- Also refactor the get and json_api_get methods so that the get method can be reused by the new proxy method. - The new proxy method makes no changes to the request to the prometheus server and response from the prometheus server. This allows it to be used as a proxy to the Prometheus server, hence the name.
-rw-r--r--app/services/prometheus/proxy_service.rb116
-rw-r--r--lib/gitlab/prometheus_client.rb51
-rw-r--r--spec/lib/gitlab/prometheus_client_spec.rb79
-rw-r--r--spec/services/prometheus/proxy_service_spec.rb195
4 files changed, 419 insertions, 22 deletions
diff --git a/app/services/prometheus/proxy_service.rb b/app/services/prometheus/proxy_service.rb
new file mode 100644
index 00000000000..c5d2b84878b
--- /dev/null
+++ b/app/services/prometheus/proxy_service.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+module Prometheus
+ class ProxyService < BaseService
+ include ReactiveCaching
+ include Gitlab::Utils::StrongMemoize
+
+ self.reactive_cache_key = ->(service) { service.cache_key }
+ self.reactive_cache_lease_timeout = 30.seconds
+ self.reactive_cache_refresh_interval = 30.seconds
+ self.reactive_cache_lifetime = 1.minute
+ self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
+
+ attr_accessor :proxyable, :method, :path, :params
+
+ PROXY_SUPPORT = {
+ 'query' => {
+ method: ['GET'],
+ params: %w(query time timeout)
+ },
+ 'query_range' => {
+ method: ['GET'],
+ params: %w(query start end step timeout)
+ }
+ }.freeze
+
+ def self.from_cache(proxyable_class_name, proxyable_id, method, path, params)
+ proxyable_class = begin
+ proxyable_class_name.constantize
+ rescue NameError
+ nil
+ end
+ return unless proxyable_class
+
+ proxyable = proxyable_class.find(proxyable_id)
+
+ new(proxyable, method, path, params)
+ end
+
+ # proxyable can be any model which responds to .prometheus_adapter
+ # like Environment.
+ def initialize(proxyable, method, path, params)
+ @proxyable = proxyable
+ @path = path
+
+ # Convert ActionController::Parameters to hash because reactive_cache_worker
+ # does not play nice with ActionController::Parameters.
+ @params = filter_params(params, path).to_hash
+
+ @method = method
+ end
+
+ def id
+ nil
+ end
+
+ def execute
+ return cannot_proxy_response unless can_proxy?
+ return no_prometheus_response unless can_query?
+
+ with_reactive_cache(*cache_key) do |result|
+ result
+ end
+ end
+
+ def calculate_reactive_cache(proxyable_class_name, proxyable_id, method, path, params)
+ return no_prometheus_response unless can_query?
+
+ response = prometheus_client_wrapper.proxy(path, params)
+
+ success(http_status: response.code, body: response.body)
+ rescue Gitlab::PrometheusClient::Error => err
+ service_unavailable_response(err)
+ end
+
+ def cache_key
+ [@proxyable.class.name, @proxyable.id, @method, @path, @params]
+ end
+
+ private
+
+ def service_unavailable_response(exception)
+ error(exception.message, :service_unavailable)
+ end
+
+ def no_prometheus_response
+ error('No prometheus server found', :service_unavailable)
+ end
+
+ def cannot_proxy_response
+ error('Proxy support for this API is not available currently')
+ end
+
+ def prometheus_adapter
+ strong_memoize(:prometheus_adapter) do
+ @proxyable.prometheus_adapter
+ end
+ end
+
+ def prometheus_client_wrapper
+ prometheus_adapter&.prometheus_client_wrapper
+ end
+
+ def can_query?
+ prometheus_adapter&.can_query?
+ end
+
+ def filter_params(params, path)
+ params.slice(*PROXY_SUPPORT.dig(path, :params))
+ end
+
+ def can_proxy?
+ PROXY_SUPPORT.dig(@path, :method)&.include?(@method)
+ end
+ end
+end
diff --git a/lib/gitlab/prometheus_client.rb b/lib/gitlab/prometheus_client.rb
index b4de7cd2bce..f13156f898e 100644
--- a/lib/gitlab/prometheus_client.rb
+++ b/lib/gitlab/prometheus_client.rb
@@ -24,6 +24,19 @@ module Gitlab
json_api_get('query', query: '1')
end
+ def proxy(type, args)
+ path = api_path(type)
+ get(path, args)
+ rescue RestClient::ExceptionWithResponse => ex
+ if ex.response
+ ex.response
+ else
+ raise PrometheusClient::Error, "Network connection error"
+ end
+ rescue RestClient::Exception
+ raise PrometheusClient::Error, "Network connection error"
+ end
+
def query(query, time: Time.now)
get_result('vector') do
json_api_get('query', query: query, time: time.to_f)
@@ -64,22 +77,14 @@ module Gitlab
private
- def json_api_get(type, args = {})
- path = ['api', 'v1', type].join('/')
- get(path, args)
- rescue JSON::ParserError
- raise PrometheusClient::Error, 'Parsing response failed'
- rescue Errno::ECONNREFUSED
- raise PrometheusClient::Error, 'Connection refused'
+ def api_path(type)
+ ['api', 'v1', type].join('/')
end
- def get(path, args)
- response = rest_client[path].get(params: args)
+ def json_api_get(type, args = {})
+ path = api_path(type)
+ response = get(path, args)
handle_response(response)
- rescue SocketError
- raise PrometheusClient::Error, "Can't connect to #{rest_client.url}"
- rescue OpenSSL::SSL::SSLError
- raise PrometheusClient::Error, "#{rest_client.url} contains invalid SSL data"
rescue RestClient::ExceptionWithResponse => ex
if ex.response
handle_exception_response(ex.response)
@@ -90,8 +95,18 @@ module Gitlab
raise PrometheusClient::Error, "Network connection error"
end
+ def get(path, args)
+ rest_client[path].get(params: args)
+ rescue SocketError
+ raise PrometheusClient::Error, "Can't connect to #{rest_client.url}"
+ rescue OpenSSL::SSL::SSLError
+ raise PrometheusClient::Error, "#{rest_client.url} contains invalid SSL data"
+ rescue Errno::ECONNREFUSED
+ raise PrometheusClient::Error, 'Connection refused'
+ end
+
def handle_response(response)
- json_data = JSON.parse(response.body)
+ json_data = parse_json(response.body)
if response.code == 200 && json_data['status'] == 'success'
json_data['data'] || {}
else
@@ -103,7 +118,7 @@ module Gitlab
if response.code == 200 && response['status'] == 'success'
response['data'] || {}
elsif response.code == 400
- json_data = JSON.parse(response.body)
+ json_data = parse_json(response.body)
raise PrometheusClient::QueryError, json_data['error'] || 'Bad data received'
else
raise PrometheusClient::Error, "#{response.code} - #{response.body}"
@@ -114,5 +129,11 @@ module Gitlab
data = yield
data['result'] if data['resultType'] == expected_type
end
+
+ def parse_json(response_body)
+ JSON.parse(response_body)
+ rescue JSON::ParserError
+ raise PrometheusClient::Error, 'Parsing response failed'
+ end
end
end
diff --git a/spec/lib/gitlab/prometheus_client_spec.rb b/spec/lib/gitlab/prometheus_client_spec.rb
index 2517ee71f24..f15ae83a02c 100644
--- a/spec/lib/gitlab/prometheus_client_spec.rb
+++ b/spec/lib/gitlab/prometheus_client_spec.rb
@@ -60,15 +60,13 @@ describe Gitlab::PrometheusClient do
end
describe 'failure to reach a provided prometheus url' do
- let(:prometheus_url) {"https://prometheus.invalid.example.com"}
+ let(:prometheus_url) {"https://prometheus.invalid.example.com/api/v1/query?query=1"}
- subject { described_class.new(RestClient::Resource.new(prometheus_url)) }
-
- context 'exceptions are raised' do
+ shared_examples 'exceptions are raised' do
it 'raises a Gitlab::PrometheusClient::Error error when a SocketError is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, SocketError)
- expect { subject.send(:get, '/', {}) }
+ expect { subject }
.to raise_error(Gitlab::PrometheusClient::Error, "Can't connect to #{prometheus_url}")
expect(req_stub).to have_been_requested
end
@@ -76,7 +74,7 @@ describe Gitlab::PrometheusClient do
it 'raises a Gitlab::PrometheusClient::Error error when a SSLError is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, OpenSSL::SSL::SSLError)
- expect { subject.send(:get, '/', {}) }
+ expect { subject }
.to raise_error(Gitlab::PrometheusClient::Error, "#{prometheus_url} contains invalid SSL data")
expect(req_stub).to have_been_requested
end
@@ -84,11 +82,23 @@ describe Gitlab::PrometheusClient do
it 'raises a Gitlab::PrometheusClient::Error error when a RestClient::Exception is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, RestClient::Exception)
- expect { subject.send(:get, '/', {}) }
+ expect { subject }
.to raise_error(Gitlab::PrometheusClient::Error, "Network connection error")
expect(req_stub).to have_been_requested
end
end
+
+ context 'ping' do
+ subject { described_class.new(RestClient::Resource.new(prometheus_url)).ping }
+
+ it_behaves_like 'exceptions are raised'
+ end
+
+ context 'proxy' do
+ subject { described_class.new(RestClient::Resource.new(prometheus_url)).proxy('query', { query: '1' }) }
+
+ it_behaves_like 'exceptions are raised'
+ end
end
describe '#query' do
@@ -258,4 +268,59 @@ describe Gitlab::PrometheusClient do
it { is_expected.to eq(step) }
end
end
+
+ describe 'proxy' do
+ context 'get API' do
+ let(:prometheus_query) { prometheus_cpu_query('env-slug') }
+ let(:query_url) { prometheus_query_url(prometheus_query) }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ context 'when response status code is 200' do
+ it 'returns response object' do
+ req_stub = stub_prometheus_request(query_url, body: prometheus_value_body('vector'))
+
+ response = subject.proxy('query', { query: prometheus_query })
+ json_response = JSON.parse(response.body)
+
+ expect(response.code).to eq(200)
+ expect(json_response).to eq({
+ 'status' => 'success',
+ 'data' => {
+ 'resultType' => 'vector',
+ 'result' => [{ "metric" => {}, "value" => [1488772511.004, "0.000041021495238095323"] }]
+ }
+ })
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'when response status code is not 200' do
+ it 'returns response object' do
+ req_stub = stub_prometheus_request(query_url, status: 400, body: { error: 'error' })
+
+ response = subject.proxy('query', { query: prometheus_query })
+ json_response = JSON.parse(response.body)
+
+ expect(req_stub).to have_been_requested
+ expect(response.code).to eq(400)
+ expect(json_response).to eq('error' => 'error')
+ end
+ end
+
+ context 'when RestClient::Exception is raised' do
+ before do
+ stub_prometheus_request_with_exception(query_url, RestClient::Exception)
+ end
+
+ it 'raises PrometheusClient::Error' do
+ expect { subject.proxy('query', { query: prometheus_query }) }.to(
+ raise_error(Gitlab::PrometheusClient::Error, 'Network connection error')
+ )
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/prometheus/proxy_service_spec.rb b/spec/services/prometheus/proxy_service_spec.rb
new file mode 100644
index 00000000000..4bdb20de4c9
--- /dev/null
+++ b/spec/services/prometheus/proxy_service_spec.rb
@@ -0,0 +1,195 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Prometheus::ProxyService do
+ include ReactiveCachingHelpers
+
+ set(:project) { create(:project) }
+ set(:environment) { create(:environment, project: project) }
+
+ describe '#initialize' do
+ let(:params) { ActionController::Parameters.new(query: '1').permit! }
+
+ it 'initializes attributes' do
+ result = described_class.new(environment, 'GET', 'query', params)
+
+ expect(result.proxyable).to eq(environment)
+ expect(result.method).to eq('GET')
+ expect(result.path).to eq('query')
+ expect(result.params).to eq('query' => '1')
+ end
+
+ it 'converts ActionController::Parameters into hash' do
+ result = described_class.new(environment, 'GET', 'query', params)
+
+ expect(result.params).to be_an_instance_of(Hash)
+ end
+
+ context 'with unknown params' do
+ let(:params) { ActionController::Parameters.new(query: '1', other_param: 'val').permit! }
+
+ it 'filters unknown params' do
+ result = described_class.new(environment, 'GET', 'query', params)
+
+ expect(result.params).to eq('query' => '1')
+ end
+ end
+ end
+
+ describe '#execute' do
+ let(:prometheus_adapter) { instance_double(PrometheusService) }
+ let(:params) { ActionController::Parameters.new(query: '1').permit! }
+
+ subject { described_class.new(environment, 'GET', 'query', params) }
+
+ context 'when prometheus_adapter is nil' do
+ before do
+ allow(environment).to receive(:prometheus_adapter).and_return(nil)
+ end
+
+ it 'returns error' do
+ expect(subject.execute).to eq(
+ status: :error,
+ message: 'No prometheus server found',
+ http_status: :service_unavailable
+ )
+ end
+ end
+
+ context 'when prometheus_adapter cannot query' do
+ before do
+ allow(environment).to receive(:prometheus_adapter).and_return(prometheus_adapter)
+ allow(prometheus_adapter).to receive(:can_query?).and_return(false)
+ end
+
+ it 'returns error' do
+ expect(subject.execute).to eq(
+ status: :error,
+ message: 'No prometheus server found',
+ http_status: :service_unavailable
+ )
+ end
+ end
+
+ context 'cannot proxy' do
+ subject { described_class.new(environment, 'POST', 'garbage', params) }
+
+ it 'returns error' do
+ expect(subject.execute).to eq(
+ message: 'Proxy support for this API is not available currently',
+ status: :error
+ )
+ end
+ end
+
+ context 'with caching', :use_clean_rails_memory_store_caching do
+ let(:return_value) { { 'http_status' => 200, 'body' => 'body' } }
+
+ let(:opts) do
+ [environment.class.name, environment.id, 'GET', 'query', { 'query' => '1' }]
+ end
+
+ before do
+ allow(environment).to receive(:prometheus_adapter)
+ .and_return(prometheus_adapter)
+ allow(prometheus_adapter).to receive(:can_query?).and_return(true)
+ end
+
+ context 'when value present in cache' do
+ before do
+ stub_reactive_cache(subject, return_value, opts)
+ end
+
+ it 'returns cached value' do
+ result = subject.execute
+
+ expect(result[:http_status]).to eq(return_value[:http_status])
+ expect(result[:body]).to eq(return_value[:body])
+ end
+ end
+
+ context 'when value not present in cache' do
+ it 'returns nil' do
+ expect(ReactiveCachingWorker)
+ .to receive(:perform_async)
+ .with(subject.class, subject.id, *opts)
+
+ result = subject.execute
+
+ expect(result).to eq(nil)
+ end
+ end
+ end
+
+ context 'call prometheus api' do
+ let(:prometheus_client) { instance_double(Gitlab::PrometheusClient) }
+
+ before do
+ synchronous_reactive_cache(subject)
+
+ allow(environment).to receive(:prometheus_adapter)
+ .and_return(prometheus_adapter)
+ allow(prometheus_adapter).to receive(:can_query?).and_return(true)
+ allow(prometheus_adapter).to receive(:prometheus_client_wrapper)
+ .and_return(prometheus_client)
+ end
+
+ context 'connection to prometheus server succeeds' do
+ let(:rest_client_response) { instance_double(RestClient::Response) }
+ let(:prometheus_http_status_code) { 400 }
+
+ let(:response_body) do
+ '{"status":"error","errorType":"bad_data","error":"parse error at char 1: no expression found in input"}'
+ end
+
+ before do
+ allow(prometheus_client).to receive(:proxy).and_return(rest_client_response)
+
+ allow(rest_client_response).to receive(:code)
+ .and_return(prometheus_http_status_code)
+ allow(rest_client_response).to receive(:body).and_return(response_body)
+ end
+
+ it 'returns the http status code and body from prometheus' do
+ expect(subject.execute).to eq(
+ http_status: prometheus_http_status_code,
+ body: response_body,
+ status: :success
+ )
+ end
+ end
+
+ context 'connection to prometheus server fails' do
+ context 'prometheus client raises Gitlab::PrometheusClient::Error' do
+ before do
+ allow(prometheus_client).to receive(:proxy)
+ .and_raise(Gitlab::PrometheusClient::Error, 'Network connection error')
+ end
+
+ it 'returns error' do
+ expect(subject.execute).to eq(
+ status: :error,
+ message: 'Network connection error',
+ http_status: :service_unavailable
+ )
+ end
+ end
+ end
+ end
+ end
+
+ describe '.from_cache' do
+ it 'initializes an instance of ProxyService class' do
+ result = described_class.from_cache(
+ environment.class.name, environment.id, 'GET', 'query', { 'query' => '1' }
+ )
+
+ expect(result).to be_an_instance_of(described_class)
+ expect(result.proxyable).to eq(environment)
+ expect(result.method).to eq('GET')
+ expect(result.path).to eq('query')
+ expect(result.params).to eq('query' => '1')
+ end
+ end
+end