Skip to content

Commit c1d9acd

Browse files
author
Daniel Magliola
authored
Merge pull request #172 from stefansundin/add-most_recent-aggregation
Add :most_recent aggregation to DirectFileStore
2 parents 204d8c9 + 826b32e commit c1d9acd

File tree

3 files changed

+131
-29
lines changed

3 files changed

+131
-29
lines changed

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -325,9 +325,9 @@ When instantiating metrics, there is an optional `store_settings` attribute. Thi
325325
to set up store-specific settings for each metric. For most stores, this is not used, but
326326
for multi-process stores, this is used to specify how to aggregate the values of each
327327
metric across multiple processes. For the most part, this is used for Gauges, to specify
328-
whether you want to report the `SUM`, `MAX` or `MIN` value observed across all processes.
329-
For almost all other cases, you'd leave the default (`SUM`). More on this on the
330-
*Aggregation* section below.
328+
whether you want to report the `SUM`, `MAX`, `MIN`, or `MOST_RECENT` value observed across
329+
all processes. For almost all other cases, you'd leave the default (`SUM`). More on this
330+
on the *Aggregation* section below.
331331

332332
Custom stores may also accept extra parameters besides `:aggregation`. See the
333333
documentation of each store for more details.
@@ -360,8 +360,11 @@ use case, you may need to control how this works. When using this store,
360360
each Metric allows you to specify an `:aggregation` setting, defining how
361361
to aggregate the multiple possible values we can get for each labelset. By default,
362362
Counters, Histograms and Summaries are `SUM`med, and Gauges report all their values (one
363-
for each process), tagged with a `pid` label. You can also select `SUM`, `MAX` or `MIN`
364-
for your gauges, depending on your use case.
363+
for each process), tagged with a `pid` label. You can also select `SUM`, `MAX`, `MIN`, or
364+
`MOST_RECENT` for your gauges, depending on your use case.
365+
366+
Please note that that the `MOST_RECENT` aggregation only works for gauges, and it does not
367+
allow the use of `increment` / `decrement`, you can only use `set`.
365368

366369
**Memory Usage**: When scraped by Prometheus, this store will read all these files, get all
367370
the values and aggregate them. We have notice this can have a noticeable effect on memory

lib/prometheus/client/data_stores/direct_file_store.rb

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ module DataStores
2929

3030
class DirectFileStore
3131
class InvalidStoreSettingsError < StandardError; end
32-
AGGREGATION_MODES = [MAX = :max, MIN = :min, SUM = :sum, ALL = :all]
32+
AGGREGATION_MODES = [MAX = :max, MIN = :min, SUM = :sum, ALL = :all, MOST_RECENT = :most_recent]
3333
DEFAULT_METRIC_SETTINGS = { aggregation: SUM }
3434
DEFAULT_GAUGE_SETTINGS = { aggregation: ALL }
3535

@@ -45,7 +45,7 @@ def for_metric(metric_name, metric_type:, metric_settings: {})
4545
end
4646

4747
settings = default_settings.merge(metric_settings)
48-
validate_metric_settings(settings)
48+
validate_metric_settings(metric_type, settings)
4949

5050
MetricStore.new(metric_name: metric_name,
5151
store_settings: @store_settings,
@@ -54,7 +54,7 @@ def for_metric(metric_name, metric_type:, metric_settings: {})
5454

5555
private
5656

57-
def validate_metric_settings(metric_settings)
57+
def validate_metric_settings(metric_type, metric_settings)
5858
unless metric_settings.has_key?(:aggregation) &&
5959
AGGREGATION_MODES.include?(metric_settings[:aggregation])
6060
raise InvalidStoreSettingsError,
@@ -65,6 +65,11 @@ def validate_metric_settings(metric_settings)
6565
raise InvalidStoreSettingsError,
6666
"Only :aggregation setting can be specified"
6767
end
68+
69+
if metric_settings[:aggregation] == MOST_RECENT && metric_type != :gauge
70+
raise InvalidStoreSettingsError,
71+
"Only :gauge metrics support :most_recent aggregation"
72+
end
6873
end
6974

7075
class MetricStore
@@ -100,6 +105,12 @@ def set(labels:, val:)
100105
end
101106

102107
def increment(labels:, by: 1)
108+
if @values_aggregation_mode == DirectFileStore::MOST_RECENT
109+
raise InvalidStoreSettingsError,
110+
"The :most_recent aggregation does not support the use of increment"\
111+
"/decrement"
112+
end
113+
103114
key = store_key(labels)
104115
in_process_sync do
105116
value = internal_store.read_value(key)
@@ -121,15 +132,15 @@ def all_values
121132
stores_for_metric.each do |file_path|
122133
begin
123134
store = FileMappedDict.new(file_path, true)
124-
store.all_values.each do |(labelset_qs, v)|
135+
store.all_values.each do |(labelset_qs, v, ts)|
125136
# Labels come as a query string, and CGI::parse returns arrays for each key
126137
# "foo=bar&x=y" => { "foo" => ["bar"], "x" => ["y"] }
127138
# Turn the keys back into symbols, and remove the arrays
128139
label_set = CGI::parse(labelset_qs).map do |k, vs|
129140
[k.to_sym, vs.first]
130141
end.to_h
131142

132-
stores_data[label_set] << v
143+
stores_data[label_set] << [v, ts]
133144
end
134145
ensure
135146
store.close if store
@@ -181,30 +192,41 @@ def process_id
181192
end
182193

183194
def aggregate_values(values)
184-
if @values_aggregation_mode == SUM
185-
values.inject { |sum, element| sum + element }
186-
elsif @values_aggregation_mode == MAX
187-
values.max
188-
elsif @values_aggregation_mode == MIN
189-
values.min
190-
elsif @values_aggregation_mode == ALL
191-
values.first
195+
# Each entry in the `values` array is a tuple of `value` and `timestamp`,
196+
# so for all aggregations except `MOST_RECENT`, we need to only take the
197+
# first value in each entry and ignore the second.
198+
if @values_aggregation_mode == MOST_RECENT
199+
latest_tuple = values.max { |a,b| a[1] <=> b[1] }
200+
latest_tuple.first # return the value without the timestamp
192201
else
193-
raise InvalidStoreSettingsError,
194-
"Invalid Aggregation Mode: #{ @values_aggregation_mode }"
202+
values = values.map(&:first) # Discard timestamps
203+
204+
if @values_aggregation_mode == SUM
205+
values.inject { |sum, element| sum + element }
206+
elsif @values_aggregation_mode == MAX
207+
values.max
208+
elsif @values_aggregation_mode == MIN
209+
values.min
210+
elsif @values_aggregation_mode == ALL
211+
values.first
212+
else
213+
raise InvalidStoreSettingsError,
214+
"Invalid Aggregation Mode: #{ @values_aggregation_mode }"
215+
end
195216
end
196217
end
197218
end
198219

199220
private_constant :MetricStore
200221

201-
# A dict of doubles, backed by an file we access directly a a byte array.
222+
# A dict of doubles, backed by an file we access directly as a byte array.
202223
#
203224
# The file starts with a 4 byte int, indicating how much of it is used.
204225
# Then 4 bytes of padding.
205226
# There's then a number of entries, consisting of a 4 byte int which is the
206227
# size of the next field, a utf-8 encoded string key, padding to an 8 byte
207-
# alignment, and then a 8 byte float which is the value.
228+
# alignment, and then a 8 byte float which is the value, and then a 8 byte
229+
# float which is the unix timestamp when the value was set.
208230
class FileMappedDict
209231
INITIAL_FILE_SIZE = 1024*1024
210232

@@ -235,8 +257,8 @@ def all_values
235257
with_file_lock do
236258
@positions.map do |key, pos|
237259
@f.seek(pos)
238-
value = @f.read(8).unpack('d')[0]
239-
[key, value]
260+
value, timestamp = @f.read(16).unpack('dd')
261+
[key, value, timestamp]
240262
end
241263
end
242264
end
@@ -256,9 +278,10 @@ def write_value(key, value)
256278
init_value(key)
257279
end
258280

281+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
259282
pos = @positions[key]
260283
@f.seek(pos)
261-
@f.write([value].pack('d'))
284+
@f.write([value, now].pack('dd'))
262285
@f.flush
263286
end
264287

@@ -299,7 +322,7 @@ def resize_file(new_capacity)
299322
def init_value(key)
300323
# Pad to be 8-byte aligned.
301324
padded = key + (' ' * (8 - (key.length + 4) % 8))
302-
value = [padded.length, padded, 0.0].pack("lA#{padded.length}d")
325+
value = [padded.length, padded, 0.0, 0.0].pack("lA#{padded.length}dd")
303326
while @used + value.length > @capacity
304327
@capacity *= 2
305328
resize_file(@capacity)
@@ -310,7 +333,7 @@ def init_value(key)
310333
@f.seek(0)
311334
@f.write([@used].pack('l'))
312335
@f.flush
313-
@positions[key] = @used - 8
336+
@positions[key] = @used - 16
314337
end
315338

316339
# Read position of all keys. No locking is performed.
@@ -320,7 +343,7 @@ def populate_positions
320343
padded_len = @f.read(4).unpack('l')[0]
321344
key = @f.read(padded_len).unpack("A#{padded_len}")[0].strip
322345
@positions[key] = @f.pos
323-
@f.seek(8, :CUR)
346+
@f.seek(16, :CUR)
324347
end
325348
end
326349
end

spec/prometheus/client/data_stores/direct_file_store_spec.rb

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
it_behaves_like Prometheus::Client::DataStores
1616

17-
it "only accepts valid :aggregation as Metric Settings" do
17+
it "only accepts valid :aggregation values as Metric Settings" do
1818
expect do
1919
subject.for_metric(:metric_name,
2020
metric_type: :counter,
@@ -26,14 +26,43 @@
2626
metric_type: :counter,
2727
metric_settings: { aggregation: :invalid })
2828
end.to raise_error(Prometheus::Client::DataStores::DirectFileStore::InvalidStoreSettingsError)
29+
end
2930

31+
it "only accepts valid keys as Metric Settings" do
32+
# the only valid key at the moment is :aggregation
3033
expect do
3134
subject.for_metric(:metric_name,
3235
metric_type: :counter,
3336
metric_settings: { some_setting: true })
3437
end.to raise_error(Prometheus::Client::DataStores::DirectFileStore::InvalidStoreSettingsError)
3538
end
3639

40+
it "only accepts :most_recent aggregation for gauges" do
41+
expect do
42+
subject.for_metric(:metric_name,
43+
metric_type: :gauge,
44+
metric_settings: { aggregation: Prometheus::Client::DataStores::DirectFileStore::MOST_RECENT })
45+
end.not_to raise_error
46+
47+
expect do
48+
subject.for_metric(:metric_name,
49+
metric_type: :counter,
50+
metric_settings: { aggregation: Prometheus::Client::DataStores::DirectFileStore::MOST_RECENT })
51+
end.to raise_error(Prometheus::Client::DataStores::DirectFileStore::InvalidStoreSettingsError)
52+
53+
expect do
54+
subject.for_metric(:metric_name,
55+
metric_type: :histogram,
56+
metric_settings: { aggregation: Prometheus::Client::DataStores::DirectFileStore::MOST_RECENT })
57+
end.to raise_error(Prometheus::Client::DataStores::DirectFileStore::InvalidStoreSettingsError)
58+
59+
expect do
60+
subject.for_metric(:metric_name,
61+
metric_type: :summary,
62+
metric_settings: { aggregation: Prometheus::Client::DataStores::DirectFileStore::MOST_RECENT })
63+
end.to raise_error(Prometheus::Client::DataStores::DirectFileStore::InvalidStoreSettingsError)
64+
end
65+
3766
it "raises when aggregating if we get to that that point with an invalid aggregation mode" do
3867
# This is basically just for coverage of a safety clause that can never be reached
3968
allow(subject).to receive(:validate_metric_settings) # turn off validation
@@ -267,6 +296,53 @@
267296
end
268297
end
269298

299+
context "with a metric that takes MOST_RECENT instead of SUM" do
300+
it "reports the most recently written value from different processes" do
301+
metric_store1 = subject.for_metric(
302+
:metric_name,
303+
metric_type: :gauge,
304+
metric_settings: { aggregation: :most_recent }
305+
)
306+
metric_store2 = subject.for_metric(
307+
:metric_name,
308+
metric_type: :gauge,
309+
metric_settings: { aggregation: :most_recent }
310+
)
311+
312+
allow(Process).to receive(:pid).and_return(12345)
313+
metric_store1.set(labels: { foo: "bar" }, val: 1)
314+
315+
allow(Process).to receive(:pid).and_return(23456)
316+
metric_store2.set(labels: { foo: "bar" }, val: 3) # Supercedes 'bar' in PID 12345
317+
metric_store2.set(labels: { foo: "baz" }, val: 2)
318+
metric_store2.set(labels: { foo: "zzz" }, val: 1)
319+
320+
allow(Process).to receive(:pid).and_return(12345)
321+
metric_store1.set(labels: { foo: "baz" }, val: 4) # Supercedes 'baz' in PID 23456
322+
323+
expect(metric_store1.all_values).to eq(
324+
{ foo: "bar" } => 3.0,
325+
{ foo: "baz" } => 4.0,
326+
{ foo: "zzz" } => 1.0,
327+
)
328+
329+
# Both processes should return the same value
330+
expect(metric_store1.all_values).to eq(metric_store2.all_values)
331+
end
332+
333+
it "does now allow `increment`, only `set`" do
334+
metric_store1 = subject.for_metric(
335+
:metric_name,
336+
metric_type: :gauge,
337+
metric_settings: { aggregation: :most_recent }
338+
)
339+
340+
expect do
341+
metric_store1.increment(labels: {})
342+
end.to raise_error(Prometheus::Client::DataStores::DirectFileStore::InvalidStoreSettingsError)
343+
end
344+
end
345+
270346
it "resizes the File if metrics get too big" do
271347
truncate_calls_count = 0
272348
allow_any_instance_of(Prometheus::Client::DataStores::DirectFileStore::FileMappedDict).

0 commit comments

Comments
 (0)