prometheus-client 0.9.0 → 0.10.0.pre.alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,313 @@
1
+ require 'concurrent'
2
+ require 'fileutils'
3
+ require "cgi"
4
+
5
+ module Prometheus
6
+ module Client
7
+ module DataStores
8
+ # Stores data in binary files, one file per process and per metric.
9
+ # This is generally the recommended store to use to deal with pre-fork servers and
10
+ # other "multi-process" scenarios.
11
+ #
12
+ # Each process will get a file for a metric, and it will manage its contents by
13
+ # storing keys next to binary-encoded Floats, and keeping track of the offsets of
14
+ # those Floats, to be able to update them directly as they increase.
15
+ #
16
+ # When exporting metrics, the process that gets scraped by Prometheus will find
17
+ # all the files that apply to a metric, read their contents, and aggregate them
18
+ # (generally that means SUMming the values for each labelset).
19
+ #
20
+ # In order to do this, each Metric needs an `:aggregation` setting, specifying how
21
+ # to aggregate the multiple possible values we can get for each labelset. By default,
22
+ # they are `SUM`med, which is what most use cases call for (counters and histograms,
23
+ # for example).
24
+ # However, for Gauges, it's possible to set `MAX` or `MIN` as aggregation, to get
25
+ # the highest value of all the processes / threads.
26
+
27
+ class DirectFileStore
28
+ class InvalidStoreSettingsError < StandardError; end
29
+ AGGREGATION_MODES = [MAX = :max, MIN = :min, SUM = :sum]
30
+ DEFAULT_METRIC_SETTINGS = { aggregation: SUM }
31
+
32
+ def initialize(dir:)
33
+ @store_settings = { dir: dir }
34
+ FileUtils.mkdir_p(dir)
35
+ end
36
+
37
+ def for_metric(metric_name, metric_type:, metric_settings: {})
38
+ settings = DEFAULT_METRIC_SETTINGS.merge(metric_settings)
39
+ validate_metric_settings(settings)
40
+
41
+ MetricStore.new(metric_name: metric_name,
42
+ store_settings: @store_settings,
43
+ metric_settings: settings)
44
+ end
45
+
46
+ private
47
+
48
+ def validate_metric_settings(metric_settings)
49
+ unless metric_settings.has_key?(:aggregation) &&
50
+ AGGREGATION_MODES.include?(metric_settings[:aggregation])
51
+ raise InvalidStoreSettingsError,
52
+ "Metrics need a valid :aggregation key"
53
+ end
54
+
55
+ unless (metric_settings.keys - [:aggregation]).empty?
56
+ raise InvalidStoreSettingsError,
57
+ "Only :aggregation setting can be specified"
58
+ end
59
+ end
60
+
61
+ class MetricStore
62
+ attr_reader :metric_name, :store_settings
63
+
64
+ def initialize(metric_name:, store_settings:, metric_settings:)
65
+ @metric_name = metric_name
66
+ @store_settings = store_settings
67
+ @values_aggregation_mode = metric_settings[:aggregation]
68
+
69
+ @rwlock = Concurrent::ReentrantReadWriteLock.new
70
+ end
71
+
72
+ # Synchronize is used to do a multi-process Mutex, when incrementing multiple
73
+ # values at once, so that the other process, reading the file for export, doesn't
74
+ # get incomplete increments.
75
+ #
76
+ # `in_process_sync`, instead, is just used so that two threads don't increment
77
+ # the same value and get a context switch between read and write leading to an
78
+ # inconsistency
79
+ def synchronize
80
+ in_process_sync do
81
+ internal_store.with_file_lock do
82
+ yield
83
+ end
84
+ end
85
+ end
86
+
87
+ def set(labels:, val:)
88
+ in_process_sync do
89
+ internal_store.write_value(store_key(labels), val.to_f)
90
+ end
91
+ end
92
+
93
+ def increment(labels:, by: 1)
94
+ key = store_key(labels)
95
+ in_process_sync do
96
+ value = internal_store.read_value(key)
97
+ internal_store.write_value(key, value + by.to_f)
98
+ end
99
+ end
100
+
101
+ def get(labels:)
102
+ in_process_sync do
103
+ internal_store.read_value(store_key(labels))
104
+ end
105
+ end
106
+
107
+ def all_values
108
+ stores_data = Hash.new{ |hash, key| hash[key] = [] }
109
+
110
+ # There's no need to call `synchronize` here. We're opening a second handle to
111
+ # the file, and `flock`ing it, which prevents inconsistent reads
112
+ stores_for_metric.each do |file_path|
113
+ begin
114
+ store = FileMappedDict.new(file_path, true)
115
+ store.all_values.each do |(labelset_qs, v)|
116
+ # Labels come as a query string, and CGI::parse returns arrays for each key
117
+ # "foo=bar&x=y" => { "foo" => ["bar"], "x" => ["y"] }
118
+ # Turn the keys back into symbols, and remove the arrays
119
+ label_set = CGI::parse(labelset_qs).map do |k, vs|
120
+ [k.to_sym, vs.first]
121
+ end.to_h
122
+
123
+ stores_data[label_set] << v
124
+ end
125
+ ensure
126
+ store.close if store
127
+ end
128
+ end
129
+
130
+ # Aggregate all the different values for each label_set
131
+ stores_data.each_with_object({}) do |(label_set, values), acc|
132
+ acc[label_set] = aggregate_values(values)
133
+ end
134
+ end
135
+
136
+ private
137
+
138
+ def in_process_sync
139
+ @rwlock.with_write_lock { yield }
140
+ end
141
+
142
+ def store_key(labels)
143
+ labels.map{|k,v| "#{CGI::escape(k.to_s)}=#{CGI::escape(v.to_s)}"}.join('&')
144
+ end
145
+
146
+ def internal_store
147
+ @internal_store ||= FileMappedDict.new(filemap_filename)
148
+ end
149
+
150
+ # Filename for this metric's PStore (one per process)
151
+ def filemap_filename
152
+ filename = "metric_#{ metric_name }___#{ process_id }.bin"
153
+ File.join(@store_settings[:dir], filename)
154
+ end
155
+
156
+ def stores_for_metric
157
+ Dir.glob(File.join(@store_settings[:dir], "metric_#{ metric_name }___*"))
158
+ end
159
+
160
+ def process_id
161
+ Process.pid
162
+ end
163
+
164
+ def aggregate_values(values)
165
+ if @values_aggregation_mode == SUM
166
+ values.inject { |sum, element| sum + element }
167
+ elsif @values_aggregation_mode == MAX
168
+ values.max
169
+ elsif @values_aggregation_mode == MIN
170
+ values.min
171
+ else
172
+ raise InvalidStoreSettingsError,
173
+ "Invalid Aggregation Mode: #{ @values_aggregation_mode }"
174
+ end
175
+ end
176
+ end
177
+
178
+ private_constant :MetricStore
179
+
180
+ # A dict of doubles, backed by an file we access directly a a byte array.
181
+ #
182
+ # The file starts with a 4 byte int, indicating how much of it is used.
183
+ # Then 4 bytes of padding.
184
+ # There's then a number of entries, consisting of a 4 byte int which is the
185
+ # size of the next field, a utf-8 encoded string key, padding to an 8 byte
186
+ # alignment, and then a 8 byte float which is the value.
187
+ class FileMappedDict
188
+ INITIAL_FILE_SIZE = 1024*1024
189
+
190
+ attr_reader :capacity, :used, :positions
191
+
192
+ def initialize(filename, readonly = false)
193
+ @positions = {}
194
+ @used = 0
195
+
196
+ open_file(filename, readonly)
197
+ @used = @f.read(4).unpack('l')[0] if @capacity > 0
198
+
199
+ if @used > 0
200
+ # File already has data. Read the existing values
201
+ with_file_lock do
202
+ read_all_values.each do |key, _, pos|
203
+ @positions[key] = pos
204
+ end
205
+ end
206
+ else
207
+ # File is empty. Init the `used` counter, if we're in write mode
208
+ if !readonly
209
+ @used = 8
210
+ @f.seek(0)
211
+ @f.write([@used].pack('l'))
212
+ end
213
+ end
214
+ end
215
+
216
+ # Yield (key, value, pos). No locking is performed.
217
+ def all_values
218
+ with_file_lock do
219
+ read_all_values.map { |k, v, p| [k, v] }
220
+ end
221
+ end
222
+
223
+ def read_value(key)
224
+ if !@positions.has_key?(key)
225
+ init_value(key)
226
+ end
227
+
228
+ pos = @positions[key]
229
+ @f.seek(pos)
230
+ @f.read(8).unpack('d')[0]
231
+ end
232
+
233
+ def write_value(key, value)
234
+ if !@positions.has_key?(key)
235
+ init_value(key)
236
+ end
237
+
238
+ pos = @positions[key]
239
+ @f.seek(pos)
240
+ @f.write([value].pack('d'))
241
+ @f.flush
242
+ end
243
+
244
+ def close
245
+ @f.close
246
+ end
247
+
248
+ def with_file_lock
249
+ @f.flock(File::LOCK_EX)
250
+ yield
251
+ ensure
252
+ @f.flock(File::LOCK_UN)
253
+ end
254
+
255
+ private
256
+
257
+ def open_file(filename, readonly)
258
+ mode = if readonly
259
+ "r"
260
+ elsif File.exist?(filename)
261
+ "r+b"
262
+ else
263
+ "w+b"
264
+ end
265
+
266
+ @f = File.open(filename, mode)
267
+ if @f.size == 0 && !readonly
268
+ resize_file(INITIAL_FILE_SIZE)
269
+ end
270
+ @capacity = @f.size
271
+ end
272
+
273
+ def resize_file(new_capacity)
274
+ @f.truncate(new_capacity)
275
+ end
276
+
277
+ # Initialize a value. Lock must be held by caller.
278
+ def init_value(key)
279
+ # Pad to be 8-byte aligned.
280
+ padded = key + (' ' * (8 - (key.length + 4) % 8))
281
+ value = [padded.length, padded, 0.0].pack("lA#{padded.length}d")
282
+ while @used + value.length > @capacity
283
+ @capacity *= 2
284
+ resize_file(@capacity)
285
+ end
286
+ @f.seek(@used)
287
+ @f.write(value)
288
+ @used += value.length
289
+ @f.seek(0)
290
+ @f.write([@used].pack('l'))
291
+ @f.flush
292
+ @positions[key] = @used - 8
293
+ end
294
+
295
+ # Yield (key, value, pos). No locking is performed.
296
+ def read_all_values
297
+ @f.seek(8)
298
+ values = []
299
+ while @f.pos < @used
300
+ padded_len = @f.read(4).unpack('l')[0]
301
+ encoded = @f.read(padded_len).unpack("A#{padded_len}")[0]
302
+ value = @f.read(8).unpack('d')[0]
303
+ values << [encoded.strip, value, @f.pos - 8]
304
+ end
305
+ values
306
+ end
307
+ end
308
+ end
309
+ end
310
+ end
311
+ end
312
+
313
+
@@ -0,0 +1,58 @@
1
+ require 'concurrent'
2
+
3
+ module Prometheus
4
+ module Client
5
+ module DataStores
6
+ # Stores all the data in a simple Hash for each Metric
7
+ #
8
+ # Has *no* synchronization primitives, making it the fastest store for single-threaded
9
+ # scenarios, but must absolutely not be used in multi-threaded scenarios.
10
+ class SingleThreaded
11
+ class InvalidStoreSettingsError < StandardError; end
12
+
13
+ def for_metric(metric_name, metric_type:, metric_settings: {})
14
+ # We don't need `metric_type` or `metric_settings` for this particular store
15
+ validate_metric_settings(metric_settings: metric_settings)
16
+ MetricStore.new
17
+ end
18
+
19
+ private
20
+
21
+ def validate_metric_settings(metric_settings:)
22
+ unless metric_settings.empty?
23
+ raise InvalidStoreSettingsError,
24
+ "SingleThreaded doesn't allow any metric_settings"
25
+ end
26
+ end
27
+
28
+ class MetricStore
29
+ def initialize
30
+ @internal_store = Hash.new { |hash, key| hash[key] = 0.0 }
31
+ end
32
+
33
+ def synchronize
34
+ yield
35
+ end
36
+
37
+ def set(labels:, val:)
38
+ @internal_store[labels] = val.to_f
39
+ end
40
+
41
+ def increment(labels:, by: 1)
42
+ @internal_store[labels] += by
43
+ end
44
+
45
+ def get(labels:)
46
+ @internal_store[labels]
47
+ end
48
+
49
+ def all_values
50
+ @internal_store.dup
51
+ end
52
+ end
53
+
54
+ private_constant :MetricStore
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,64 @@
1
+ require 'concurrent'
2
+
3
+ module Prometheus
4
+ module Client
5
+ module DataStores
6
+ # Stores all the data in simple hashes, one per metric. Each of these metrics
7
+ # synchronizes access to their hash, but multiple metrics can run observations
8
+ # concurrently.
9
+ class Synchronized
10
+ class InvalidStoreSettingsError < StandardError; end
11
+
12
+ def for_metric(metric_name, metric_type:, metric_settings: {})
13
+ # We don't need `metric_type` or `metric_settings` for this particular store
14
+ validate_metric_settings(metric_settings: metric_settings)
15
+ MetricStore.new
16
+ end
17
+
18
+ private
19
+
20
+ def validate_metric_settings(metric_settings:)
21
+ unless metric_settings.empty?
22
+ raise InvalidStoreSettingsError,
23
+ "Synchronized doesn't allow any metric_settings"
24
+ end
25
+ end
26
+
27
+ class MetricStore
28
+ def initialize
29
+ @internal_store = Hash.new { |hash, key| hash[key] = 0.0 }
30
+ @rwlock = Concurrent::ReentrantReadWriteLock.new
31
+ end
32
+
33
+ def synchronize
34
+ @rwlock.with_write_lock { yield }
35
+ end
36
+
37
+ def set(labels:, val:)
38
+ synchronize do
39
+ @internal_store[labels] = val.to_f
40
+ end
41
+ end
42
+
43
+ def increment(labels:, by: 1)
44
+ synchronize do
45
+ @internal_store[labels] += by
46
+ end
47
+ end
48
+
49
+ def get(labels:)
50
+ synchronize do
51
+ @internal_store[labels]
52
+ end
53
+ end
54
+
55
+ def all_values
56
+ synchronize { @internal_store.dup }
57
+ end
58
+ end
59
+
60
+ private_constant :MetricStore
61
+ end
62
+ end
63
+ end
64
+ end
@@ -40,37 +40,31 @@ module Prometheus
40
40
  private
41
41
 
42
42
  def representation(metric, label_set, value, &block)
43
- set = metric.base_labels.merge(label_set)
44
-
45
43
  if metric.type == :summary
46
- summary(metric.name, set, value, &block)
44
+ summary(metric.name, label_set, value, &block)
47
45
  elsif metric.type == :histogram
48
- histogram(metric.name, set, value, &block)
46
+ histogram(metric.name, label_set, value, &block)
49
47
  else
50
- yield metric(metric.name, labels(set), value)
48
+ yield metric(metric.name, labels(label_set), value)
51
49
  end
52
50
  end
53
51
 
54
52
  def summary(name, set, value)
55
- value.each do |q, v|
56
- yield metric(name, labels(set.merge(quantile: q)), v)
57
- end
58
-
59
53
  l = labels(set)
60
- yield metric("#{name}_sum", l, value.sum)
61
- yield metric("#{name}_count", l, value.total)
54
+ yield metric("#{name}_sum", l, value["sum"])
55
+ yield metric("#{name}_count", l, value["count"])
62
56
  end
63
57
 
64
58
  def histogram(name, set, value)
65
59
  bucket = "#{name}_bucket"
66
60
  value.each do |q, v|
61
+ next if q == "sum"
67
62
  yield metric(bucket, labels(set.merge(le: q)), v)
68
63
  end
69
- yield metric(bucket, labels(set.merge(le: '+Inf')), value.total)
70
64
 
71
65
  l = labels(set)
72
- yield metric("#{name}_sum", l, value.sum)
73
- yield metric("#{name}_count", l, value.total)
66
+ yield metric("#{name}_sum", l, value["sum"])
67
+ yield metric("#{name}_count", l, value["+Inf"])
74
68
  end
75
69
 
76
70
  def metric(name, labels, value)