summaryrefslogtreecommitdiff
path: root/app/services/prometheus/proxy_service.rb
blob: c1bafd03b48be48a7be92fb8305c293396e881b9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# 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