summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorrpereira2 <rpereira@gitlab.com>2019-04-02 11:35:29 +0530
committerPeter Leitzen <pleitzen@gitlab.com>2019-04-04 22:22:44 +0200
commit399173d6fad09b8a6334d38c3b8412617fe1dd9a (patch)
treecd27f2c27d40dfdad723a60d3b9e4752a755dfb8
parentbaab836b9bb5cab5fada8e0ce155b80b805c07b7 (diff)
downloadgitlab-ce-399173d6fad09b8a6334d38c3b8412617fe1dd9a.tar.gz
Add a Prometheus::ProxyService
- The service uses the PrometheusClient.proxy method to call the Prometheus API with the given parameters, and returns the body and http status code of the API response to the caller of the service. - The service uses reactive caching in order to prevent Puma/Unicorn threads from being blocked until the Prometheus API responds.
-rw-r--r--app/services/prometheus/proxy_service.rb105
-rw-r--r--spec/services/prometheus/proxy_service_spec.rb204
2 files changed, 309 insertions, 0 deletions
diff --git a/app/services/prometheus/proxy_service.rb b/app/services/prometheus/proxy_service.rb
new file mode 100644
index 00000000000..6dc68e378fd
--- /dev/null
+++ b/app/services/prometheus/proxy_service.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+module Prometheus
+ class ProxyService < BaseService
+ include ReactiveCaching
+
+ 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 :prometheus_owner, :method, :path, :params
+
+ PROXY_SUPPORT = {
+ 'query' => 'GET',
+ 'query_range' => 'GET'
+ }.freeze
+
+ def self.from_cache(prometheus_owner_class_name, prometheus_owner_id, method, path, params)
+ prometheus_owner_class = begin
+ prometheus_owner_class_name.constantize
+ rescue NameError
+ nil
+ end
+ return unless prometheus_owner_class
+
+ prometheus_owner = prometheus_owner_class.find(prometheus_owner_id)
+
+ new(prometheus_owner, method, path, params)
+ end
+
+ # prometheus_owner can be any model which responds to .prometheus_adapter
+ # like Environment.
+ def initialize(prometheus_owner, method, path, params)
+ @prometheus_owner = prometheus_owner
+ @path = path
+ # Convert ActionController::Parameters to hash because reactive_cache_worker
+ # does not play nice with ActionController::Parameters.
+ @params = params.to_hash
+ @method = method
+ end
+
+ def id
+ nil
+ end
+
+ def execute
+ return cannot_proxy_response unless can_proxy?(@method, @path)
+ return no_prometheus_response unless can_query?
+
+ with_reactive_cache(*cache_key) do |result|
+ result
+ end
+ end
+
+ def calculate_reactive_cache(prometheus_owner_class_name, prometheus_owner_id, method, path, params)
+ @prometheus_owner = prometheus_owner_from_class(prometheus_owner_class_name, prometheus_owner_id)
+
+ return cannot_proxy_response unless can_proxy?(method, path)
+ 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
+ error(err.message, :service_unavailable)
+ end
+
+ def cache_key
+ [@prometheus_owner.class.name, @prometheus_owner.id, @method, @path, @params]
+ end
+
+ private
+
+ 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_owner_from_class(prometheus_owner_class_name, prometheus_owner_id)
+ Kernel.const_get(prometheus_owner_class_name).find(prometheus_owner_id)
+ end
+
+ def prometheus_adapter
+ @prometheus_adapter ||= @prometheus_owner.prometheus_adapter
+ end
+
+ def prometheus_client_wrapper
+ prometheus_adapter&.prometheus_client_wrapper
+ end
+
+ def can_query?
+ prometheus_adapter&.can_query?
+ end
+
+ def can_proxy?(method, path)
+ PROXY_SUPPORT[path] == method
+ 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..53e852aa79c
--- /dev/null
+++ b/spec/services/prometheus/proxy_service_spec.rb
@@ -0,0 +1,204 @@
+# 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', { query: '1' })
+
+ expect(result.prometheus_owner).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
+ end
+
+ describe '#execute' do
+ let(:prometheus_adapter) { instance_double(PrometheusService) }
+
+ subject { described_class.new(environment, 'GET', 'query', { query: '1' }) }
+
+ context 'When prometheus_adapter is nil' do
+ before do
+ allow(environment).to receive(:prometheus_adapter).and_return(nil)
+ end
+
+ it 'should return 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 'should return 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', 'query', { query: '1' }) }
+
+ it 'returns error' do
+ expect(subject.execute).to eq({
+ message: 'Proxy support for this API is not available currently',
+ status: :error
+ })
+ end
+ end
+
+ context 'When cached', :use_clean_rails_memory_store_caching do
+ let(:return_value) { { 'http_status' => 200, 'body' => 'body' } }
+ let(:opts) { [environment.class.name, environment.id, 'GET', 'query', { query: '1' }] }
+
+ before do
+ stub_reactive_cache(subject, return_value, opts)
+
+ allow(environment).to receive(:prometheus_adapter)
+ .and_return(prometheus_adapter)
+ allow(prometheus_adapter).to receive(:can_query?).and_return(true)
+ 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 not cached' do
+ let(:return_value) { { 'http_status' => 200, 'body' => 'body' } }
+ let(:opts) { [environment.class.name, environment.id, 'GET', 'query', { query: '1' }] }
+
+ before do
+ allow(environment).to receive(:prometheus_adapter)
+ .and_return(prometheus_adapter)
+ allow(prometheus_adapter).to receive(:can_query?).and_return(true)
+ end
+
+ 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
+
+ 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) }
+
+ 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
+
+ shared_examples 'return prometheus http status code and body' do
+ it do
+ expect(subject.execute).to eq({
+ http_status: prometheus_http_status_code,
+ body: response_body,
+ status: :success
+ })
+ end
+ end
+
+ context 'prometheus returns success' do
+ let(:prometheus_http_status_code) { 200 }
+
+ let(:response_body) do
+ '{"status":"success","data":{"resultType":"scalar","result":[1553864609.117,"1"]}}'
+ end
+
+ before do
+ end
+
+ it_behaves_like 'return prometheus http status code and body'
+ end
+
+ context 'prometheus returns error' do
+ 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
+
+ it_behaves_like 'return prometheus http status code and body'
+ 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.prometheus_owner).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