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
|