# frozen_string_literal: true module Prometheus class ProxyService < BaseService include ReactiveCaching include Gitlab::Utils::StrongMemoize self.reactive_cache_key = ->(service) { [] } self.reactive_cache_lease_timeout = 30.seconds # reactive_cache_refresh_interval should be set to a value higher than # reactive_cache_lifetime. If the refresh_interval is less than lifetime # then the ReactiveCachingWorker will re-query prometheus for this # PromQL query even though it's (probably) already been picked up by # the frontend # refresh_interval should be set less than lifetime only if this data # is expected to change *and* be fetched again by the frontend self.reactive_cache_refresh_interval = 90.seconds self.reactive_cache_lifetime = 1.minute self.reactive_cache_work_type = :external_dependency self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) } attr_accessor :proxyable, :method, :path, :params PROMETHEUS_QUERY_API = 'query' PROMETHEUS_QUERY_RANGE_API = 'query_range' PROMETHEUS_SERIES_API = 'series' PROXY_SUPPORT = { PROMETHEUS_QUERY_API => { method: ['GET'], params: %w(query time timeout) }, PROMETHEUS_QUERY_RANGE_API => { method: ['GET'], params: %w(query start end step timeout) }, PROMETHEUS_SERIES_API => { method: %w(GET), params: %w(match start end) } }.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 end def can_query? prometheus_adapter&.can_query? end def filter_params(params, path) params = substitute_params(params) params.slice(*PROXY_SUPPORT.dig(path, :params)) end def can_proxy? PROXY_SUPPORT.dig(@path, :method)&.include?(@method) end def substitute_params(params) start_time = params[:start_time] end_time = params[:end_time] params['start'] = start_time if start_time params['end'] = end_time if end_time params end end end