Commit 399173d6 authored by rpereira2's avatar rpereira2 Committed by Peter Leitzen

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.
parent baab836b
# 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
# 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
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