diff options
author | rpereira2 <rpereira@gitlab.com> | 2019-04-02 11:35:29 +0530 |
---|---|---|
committer | Peter Leitzen <pleitzen@gitlab.com> | 2019-04-04 22:22:44 +0200 |
commit | 399173d6fad09b8a6334d38c3b8412617fe1dd9a (patch) | |
tree | cd27f2c27d40dfdad723a60d3b9e4752a755dfb8 | |
parent | baab836b9bb5cab5fada8e0ce155b80b805c07b7 (diff) | |
download | gitlab-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.rb | 105 | ||||
-rw-r--r-- | spec/services/prometheus/proxy_service_spec.rb | 204 |
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 |