vm-client 1.0.0
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.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/README.md +554 -0
- data/lib/prometheus/client/config.rb +15 -0
- data/lib/prometheus/client/counter.rb +21 -0
- data/lib/prometheus/client/data_stores/README.md +306 -0
- data/lib/prometheus/client/data_stores/direct_file_store.rb +368 -0
- data/lib/prometheus/client/data_stores/single_threaded.rb +56 -0
- data/lib/prometheus/client/data_stores/synchronized.rb +62 -0
- data/lib/prometheus/client/formats/text.rb +106 -0
- data/lib/prometheus/client/gauge.rb +42 -0
- data/lib/prometheus/client/histogram.rb +151 -0
- data/lib/prometheus/client/label_set_validator.rb +80 -0
- data/lib/prometheus/client/metric.rb +120 -0
- data/lib/prometheus/client/push.rb +226 -0
- data/lib/prometheus/client/registry.rb +100 -0
- data/lib/prometheus/client/summary.rb +69 -0
- data/lib/prometheus/client/version.rb +7 -0
- data/lib/prometheus/client/vm_histogram.rb +164 -0
- data/lib/prometheus/client.rb +18 -0
- data/lib/prometheus/middleware/collector.rb +103 -0
- data/lib/prometheus/middleware/exporter.rb +96 -0
- data/lib/prometheus.rb +5 -0
- metadata +108 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Custom Data Stores
|
|
2
|
+
|
|
3
|
+
Stores are basically an abstraction over a Hash, whose keys are in turn a Hash of labels
|
|
4
|
+
plus a metric name. The intention behind having different data stores is solving
|
|
5
|
+
different requirements for different production scenarios, or performance trade-offs.
|
|
6
|
+
|
|
7
|
+
The most common of these scenarios are pre-fork servers like Unicorn, which have multiple
|
|
8
|
+
separate processes gathering metrics. If each of these had their own store, the metrics
|
|
9
|
+
reported on each Prometheus scrape would be different, depending on which process handles
|
|
10
|
+
the request. Solving this requires some sort of shared storage between these processes,
|
|
11
|
+
and there are many ways to solve this problem, each with their own trade-offs.
|
|
12
|
+
|
|
13
|
+
This abstraction allows us to easily plug in the most adequate store for each scenario.
|
|
14
|
+
|
|
15
|
+
## Interface
|
|
16
|
+
|
|
17
|
+
`Store` exposes a `for_metric` method, which returns a store-specific and metric-specific
|
|
18
|
+
`MetricStore` object, which represents a "view" onto the actual internal storage for one
|
|
19
|
+
particular metric. Each metric / collector object will have a references to this
|
|
20
|
+
`MetricStore` and interact with it directly.
|
|
21
|
+
|
|
22
|
+
The `MetricStore` class must expose `synchronize`, `set`, `increment`, `get` and `all_values`
|
|
23
|
+
methods, which are explained in the code sample below. Its initializer should be called
|
|
24
|
+
only by `Store#for_metric`, not directly.
|
|
25
|
+
|
|
26
|
+
All values stored are `Float`s.
|
|
27
|
+
|
|
28
|
+
Internally, a `Store` can store the data however it needs to, based on its requirements.
|
|
29
|
+
For example, a store that needs to work in a multi-process environment needs to have a
|
|
30
|
+
shared section of memory, via either Files, an MMap, an external database, or whatever the
|
|
31
|
+
implementor chooses for their particular use case.
|
|
32
|
+
|
|
33
|
+
Each `Store` / `MetricStore` will also choose how to divide responsibilities over the
|
|
34
|
+
storage of values. For some use cases, each `MetricStore` may have their own individual
|
|
35
|
+
storage, whereas for others, the `Store` may own a central storage, and `MetricStore`
|
|
36
|
+
objects will access it through the `Store`. This depends on the design choices of each `Store`.
|
|
37
|
+
|
|
38
|
+
`Store` and `MetricStore` MUST be thread safe. This applies not only to operations on
|
|
39
|
+
stored values (`set`, `increment`), but `MetricStore` must also expose a `synchronize`
|
|
40
|
+
method that would allow a Metric to increment multiple values atomically (Histograms need
|
|
41
|
+
this, for example).
|
|
42
|
+
|
|
43
|
+
Ideally, multiple keys should be modifiable simultaneously, but this is not a
|
|
44
|
+
hard requirement.
|
|
45
|
+
|
|
46
|
+
This is what the interface looks like, in practice:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
module Prometheus
|
|
50
|
+
module Client
|
|
51
|
+
module DataStores
|
|
52
|
+
class CustomStore
|
|
53
|
+
|
|
54
|
+
# Return a MetricStore, which provides a view of the internal data store,
|
|
55
|
+
# catering specifically to that metric.
|
|
56
|
+
#
|
|
57
|
+
# - `metric_settings` specifies configuration parameters for this metric
|
|
58
|
+
# specifically. These may or may not be necessary, depending on each specific
|
|
59
|
+
# store and metric. The most obvious example of this is for gauges in
|
|
60
|
+
# multi-process environments, where the developer needs to choose how those
|
|
61
|
+
# gauges will get aggregated between all the per-process values.
|
|
62
|
+
#
|
|
63
|
+
# The settings that the store will accept, and what it will do with them, are
|
|
64
|
+
# 100% Store-specific. Each store should document what settings it will accept
|
|
65
|
+
# and how to use them, so the developer using that store can pass the appropriate
|
|
66
|
+
# instantiating the Store itself, and the Metrics they declare.
|
|
67
|
+
#
|
|
68
|
+
# - `metric_type` is specified in case a store wants to validate that the settings
|
|
69
|
+
# are valid for the metric being set up. It may go unused by most Stores
|
|
70
|
+
#
|
|
71
|
+
# Even if your store doesn't need these two parameters, the Store must expose them
|
|
72
|
+
# to make them swappable.
|
|
73
|
+
def for_metric(metric_name, metric_type:, metric_settings: {})
|
|
74
|
+
# Generally, here a Store would validate that the settings passed in are valid,
|
|
75
|
+
# and raise if they aren't.
|
|
76
|
+
validate_metric_settings(metric_type: metric_type,
|
|
77
|
+
metric_settings: metric_settings)
|
|
78
|
+
MetricStore.new(store: self,
|
|
79
|
+
metric_name: metric_name,
|
|
80
|
+
metric_type: metric_type,
|
|
81
|
+
metric_settings: metric_settings)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# MetricStore manages the data for one specific metric. It's generally a view onto
|
|
86
|
+
# the central store shared by all metrics, but it could also hold the data itself
|
|
87
|
+
# if that's better for the specific scenario
|
|
88
|
+
class MetricStore
|
|
89
|
+
# This constructor is internal to this store, so the signature doesn't need to
|
|
90
|
+
# be this. No one other than the Store should be creating MetricStores
|
|
91
|
+
def initialize(store:, metric_name:, metric_type:, metric_settings:)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Metrics may need to modify multiple values at once (Histograms do this, for
|
|
95
|
+
# example). MetricStore needs to provide a way to synchronize those, in addition
|
|
96
|
+
# to all of the value modifications being thread-safe without a need for simple
|
|
97
|
+
# Metrics to call `synchronize`
|
|
98
|
+
def synchronize
|
|
99
|
+
raise NotImplementedError
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Store a value for this metric and a set of labels
|
|
103
|
+
# Internally, may add extra "labels" to disambiguate values between,
|
|
104
|
+
# for example, different processes
|
|
105
|
+
def set(labels:, val:)
|
|
106
|
+
raise NotImplementedError
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def increment(labels:, by: 1)
|
|
110
|
+
raise NotImplementedError
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Return a value for a set of labels
|
|
114
|
+
# Will return the same value stored by `set`, as opposed to `all_values`, which
|
|
115
|
+
# may aggregate multiple values.
|
|
116
|
+
#
|
|
117
|
+
# For example, in a multi-process scenario, `set` may add an extra internal
|
|
118
|
+
# label tagging the value with the process id. `get` will return the value for
|
|
119
|
+
# "this" process ID. `all_values` will return an aggregated value for all
|
|
120
|
+
# process IDs.
|
|
121
|
+
def get(labels:)
|
|
122
|
+
raise NotImplementedError
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Returns all the sets of labels seen by the Store, and the aggregated value for
|
|
126
|
+
# each.
|
|
127
|
+
#
|
|
128
|
+
# In some cases, this is just a matter of returning the stored value.
|
|
129
|
+
#
|
|
130
|
+
# In other cases, the store may need to aggregate multiple values for the same
|
|
131
|
+
# set of labels. For example, in a multiple process it may need to `sum` the
|
|
132
|
+
# values of counters from each process. Or for `gauges`, it may need to take the
|
|
133
|
+
# `max`. This is generally specified in `metric_settings` when calling
|
|
134
|
+
# `Store#for_metric`.
|
|
135
|
+
def all_values
|
|
136
|
+
raise NotImplementedError
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Conventions
|
|
146
|
+
|
|
147
|
+
- Your store MAY require or accept extra settings for each metric on the call to `for_metric`.
|
|
148
|
+
- You SHOULD validate these parameters to make sure they are correct, and raise if they aren't.
|
|
149
|
+
- If your store needs to aggregate multiple values for the same metric (for example, in
|
|
150
|
+
a multi-process scenario), you MUST accept a setting to define how values are aggregated.
|
|
151
|
+
- This setting MUST be called `:aggregation`
|
|
152
|
+
- It MUST support, at least, `:sum`, `:max` and `:min`.
|
|
153
|
+
- It MAY support other aggregation modes that may apply to your requirements.
|
|
154
|
+
- It MUST default to `:sum`
|
|
155
|
+
|
|
156
|
+
## Testing your Store
|
|
157
|
+
|
|
158
|
+
In order to make it easier to test your store, the basic functionality is tested using
|
|
159
|
+
`shared_examples`:
|
|
160
|
+
|
|
161
|
+
`it_behaves_like Prometheus::Client::DataStores`
|
|
162
|
+
|
|
163
|
+
Follow the simple structure in `synchronized_spec.rb` for a starting point.
|
|
164
|
+
|
|
165
|
+
Note that if your store stores data somewhere other than in-memory (in files, Redis,
|
|
166
|
+
databases, etc), you will need to do cleanup between tests in a `before` block.
|
|
167
|
+
|
|
168
|
+
The tests for `DirectFileStore` have a good example at the top of the file. This file also
|
|
169
|
+
has some examples on testing multi-process stores, checking that aggregation between
|
|
170
|
+
processes works correctly.
|
|
171
|
+
|
|
172
|
+
## Benchmarking your custom data store
|
|
173
|
+
|
|
174
|
+
If you are developing your own data store, you probably want to benchmark it to see how
|
|
175
|
+
it compares to the built-in ones, and to make sure it achieves the performance you want.
|
|
176
|
+
|
|
177
|
+
The Prometheus Ruby Client includes some benchmarks (in the `spec/benchmarks` directory)
|
|
178
|
+
to help you with this, and also with validating that your store works correctly.
|
|
179
|
+
|
|
180
|
+
The `README` in that directory contains more information what these benchmarks are for,
|
|
181
|
+
and how to use them.
|
|
182
|
+
|
|
183
|
+
## Extra Stores and Research
|
|
184
|
+
|
|
185
|
+
In the process of abstracting stores away, and creating the built-in ones, GoCardless
|
|
186
|
+
has created a good amount of research, benchmarks, and experimental stores, which
|
|
187
|
+
weren't useful to include in this repo, but may be a useful resource or starting point
|
|
188
|
+
if you are building your own store.
|
|
189
|
+
|
|
190
|
+
Check out the [GoCardless Data Stores Experiments](https://github.com/gocardless/prometheus-client-ruby-data-stores-experiments)
|
|
191
|
+
repository for these.
|
|
192
|
+
|
|
193
|
+
## Sample, imaginary multi-process Data Store
|
|
194
|
+
|
|
195
|
+
This is just an example of how one could implement a data store, and a clarification on
|
|
196
|
+
the "aggregation" point
|
|
197
|
+
|
|
198
|
+
Important: This is a **toy example**, intended simply to show how this could work / how to
|
|
199
|
+
implement these interfaces.
|
|
200
|
+
|
|
201
|
+
There are some key pieces of code missing, which are fairly uninteresting, this only shows
|
|
202
|
+
the parts that illustrate the idea of storing multiple different values, and aggregating
|
|
203
|
+
them
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
module Prometheus
|
|
207
|
+
module Client
|
|
208
|
+
module DataStores
|
|
209
|
+
# Stores all the data in a magic data structure that keeps cross-process data, in a
|
|
210
|
+
# way that all processes can read it, but each can write only to their own set of
|
|
211
|
+
# keys.
|
|
212
|
+
# It doesn't care how that works, this is not an actual solution to anything,
|
|
213
|
+
# just an example of how the interface would work with something like that.
|
|
214
|
+
#
|
|
215
|
+
# Metric Settings have one possible key, `aggregation`, which must be one of
|
|
216
|
+
# `AGGREGATION_MODES`
|
|
217
|
+
class SampleMagicMultiprocessStore
|
|
218
|
+
AGGREGATION_MODES = [MAX = :max, MIN = :min, SUM = :sum]
|
|
219
|
+
DEFAULT_METRIC_SETTINGS = { aggregation: SUM }
|
|
220
|
+
|
|
221
|
+
def initialize
|
|
222
|
+
@internal_store = MagicHashSharedBetweenProcesses.new # PStore, for example
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def for_metric(metric_name, metric_type:, metric_settings: {})
|
|
226
|
+
settings = DEFAULT_METRIC_SETTINGS.merge(metric_settings)
|
|
227
|
+
validate_metric_settings(metric_settings: settings)
|
|
228
|
+
MetricStore.new(store: self,
|
|
229
|
+
metric_name: metric_name,
|
|
230
|
+
metric_type: metric_type,
|
|
231
|
+
metric_settings: settings)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
private
|
|
235
|
+
|
|
236
|
+
def validate_metric_settings(metric_settings:)
|
|
237
|
+
raise unless metric_settings.has_key?(:aggregation)
|
|
238
|
+
raise unless metric_settings[:aggregation].in?(AGGREGATION_MODES)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
class MetricStore
|
|
242
|
+
def initialize(store:, metric_name:, metric_type:, metric_settings:)
|
|
243
|
+
@store = store
|
|
244
|
+
@internal_store = store.internal_store
|
|
245
|
+
@metric_name = metric_name
|
|
246
|
+
@aggregation_mode = metric_settings[:aggregation]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def set(labels:, val:)
|
|
250
|
+
@internal_store[store_key(labels)] = val.to_f
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def get(labels:)
|
|
254
|
+
@internal_store[store_key(labels)]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def all_values
|
|
258
|
+
non_aggregated_values = all_store_values.each_with_object({}) do |(labels, v), acc|
|
|
259
|
+
if labels["__metric_name"] == @metric_name
|
|
260
|
+
label_set = labels.reject { |k,_| k.in?("__metric_name", "__pid") }
|
|
261
|
+
acc[label_set] ||= []
|
|
262
|
+
acc[label_set] << v
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Aggregate all the different values for each label_set
|
|
267
|
+
non_aggregated_values.each_with_object({}) do |(label_set, values), acc|
|
|
268
|
+
acc[label_set] = aggregate(values)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
private
|
|
273
|
+
|
|
274
|
+
def all_store_values
|
|
275
|
+
# This assumes there's a something common that all processes can write to, and
|
|
276
|
+
# it's magically synchronized (which is not true of a PStore, for example, but
|
|
277
|
+
# would of some sort of external data store like Redis, Memcached, SQLite)
|
|
278
|
+
|
|
279
|
+
# This could also have some sort of:
|
|
280
|
+
# file_list = Dir.glob(File.join(path, '*.db')).sort
|
|
281
|
+
# which reads all the PStore files / MMapped files, etc, and returns a hash
|
|
282
|
+
# with all of them together, which then `values` and `label_sets` can use
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# This method holds most of the key to how this Store works. Adding `_pid` as
|
|
286
|
+
# one of the labels, we hold each process's value separately, which we can
|
|
287
|
+
# aggregate later
|
|
288
|
+
def store_key(labels)
|
|
289
|
+
labels.merge(
|
|
290
|
+
{
|
|
291
|
+
"__metric_name" => @metric_name,
|
|
292
|
+
"__pid" => Process.pid
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def aggregate(values)
|
|
298
|
+
# This is a horrible way to do this, just illustrating the point
|
|
299
|
+
values.send(@aggregation_mode)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
```
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require "cgi"
|
|
3
|
+
|
|
4
|
+
module Prometheus
|
|
5
|
+
module Client
|
|
6
|
+
module DataStores
|
|
7
|
+
# Stores data in binary files, one file per process and per metric.
|
|
8
|
+
# This is generally the recommended store to use to deal with pre-fork servers and
|
|
9
|
+
# other "multi-process" scenarios.
|
|
10
|
+
#
|
|
11
|
+
# Each process will get a file for a metric, and it will manage its contents by
|
|
12
|
+
# storing keys next to binary-encoded Floats, and keeping track of the offsets of
|
|
13
|
+
# those Floats, to be able to update them directly as they increase.
|
|
14
|
+
#
|
|
15
|
+
# When exporting metrics, the process that gets scraped by Prometheus will find
|
|
16
|
+
# all the files that apply to a metric, read their contents, and aggregate them
|
|
17
|
+
# (generally that means SUMming the values for each labelset).
|
|
18
|
+
#
|
|
19
|
+
# In order to do this, each Metric needs an `:aggregation` setting, specifying how
|
|
20
|
+
# to aggregate the multiple possible values we can get for each labelset. By default,
|
|
21
|
+
# Counters, Histograms and Summaries get `SUM`med, and Gauges will report `ALL`
|
|
22
|
+
# values, tagging each one with a `pid` label.
|
|
23
|
+
# For Gauges, it's also possible to set `SUM`, MAX` or `MIN` as aggregation, to get
|
|
24
|
+
# the highest / lowest value / or the sum of all the processes / threads.
|
|
25
|
+
#
|
|
26
|
+
# Before using this Store, please read the "`DirectFileStore` caveats and things to
|
|
27
|
+
# keep in mind" section of the main README in this repository. It includes a number
|
|
28
|
+
# of important things to keep in mind.
|
|
29
|
+
|
|
30
|
+
class DirectFileStore
|
|
31
|
+
class InvalidStoreSettingsError < StandardError; end
|
|
32
|
+
AGGREGATION_MODES = [MAX = :max, MIN = :min, SUM = :sum, ALL = :all, MOST_RECENT = :most_recent]
|
|
33
|
+
DEFAULT_METRIC_SETTINGS = { aggregation: SUM }
|
|
34
|
+
DEFAULT_GAUGE_SETTINGS = { aggregation: ALL }
|
|
35
|
+
|
|
36
|
+
def initialize(dir:)
|
|
37
|
+
@store_settings = { dir: dir }
|
|
38
|
+
FileUtils.mkdir_p(dir)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def for_metric(metric_name, metric_type:, metric_settings: {})
|
|
42
|
+
default_settings = DEFAULT_METRIC_SETTINGS
|
|
43
|
+
if metric_type == :gauge
|
|
44
|
+
default_settings = DEFAULT_GAUGE_SETTINGS
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
settings = default_settings.merge(metric_settings)
|
|
48
|
+
validate_metric_settings(metric_type, settings)
|
|
49
|
+
|
|
50
|
+
MetricStore.new(metric_name: metric_name,
|
|
51
|
+
store_settings: @store_settings,
|
|
52
|
+
metric_settings: settings)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def validate_metric_settings(metric_type, metric_settings)
|
|
58
|
+
unless metric_settings.has_key?(:aggregation) &&
|
|
59
|
+
AGGREGATION_MODES.include?(metric_settings[:aggregation])
|
|
60
|
+
raise InvalidStoreSettingsError,
|
|
61
|
+
"Metrics need a valid :aggregation key"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
unless (metric_settings.keys - [:aggregation]).empty?
|
|
65
|
+
raise InvalidStoreSettingsError,
|
|
66
|
+
"Only :aggregation setting can be specified"
|
|
67
|
+
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
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class MetricStore
|
|
76
|
+
attr_reader :metric_name, :store_settings
|
|
77
|
+
|
|
78
|
+
def initialize(metric_name:, store_settings:, metric_settings:)
|
|
79
|
+
@metric_name = metric_name
|
|
80
|
+
@store_settings = store_settings
|
|
81
|
+
@values_aggregation_mode = metric_settings[:aggregation]
|
|
82
|
+
@store_opened_by_pid = nil
|
|
83
|
+
|
|
84
|
+
@lock = Monitor.new
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Synchronize is used to do a multi-process Mutex, when incrementing multiple
|
|
88
|
+
# values at once, so that the other process, reading the file for export, doesn't
|
|
89
|
+
# get incomplete increments.
|
|
90
|
+
#
|
|
91
|
+
# `in_process_sync`, instead, is just used so that two threads don't increment
|
|
92
|
+
# the same value and get a context switch between read and write leading to an
|
|
93
|
+
# inconsistency
|
|
94
|
+
def synchronize
|
|
95
|
+
in_process_sync do
|
|
96
|
+
internal_store.with_file_lock do
|
|
97
|
+
yield
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def set(labels:, val:)
|
|
103
|
+
in_process_sync do
|
|
104
|
+
internal_store.write_value(store_key(labels), val.to_f)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def increment(labels:, by: 1)
|
|
109
|
+
if @values_aggregation_mode == DirectFileStore::MOST_RECENT
|
|
110
|
+
raise InvalidStoreSettingsError,
|
|
111
|
+
"The :most_recent aggregation does not support the use of increment"\
|
|
112
|
+
"/decrement"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
key = store_key(labels)
|
|
116
|
+
in_process_sync do
|
|
117
|
+
internal_store.increment_value(key, by.to_f)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def get(labels:)
|
|
122
|
+
in_process_sync do
|
|
123
|
+
internal_store.read_value(store_key(labels))
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def all_values
|
|
128
|
+
stores_data = Hash.new{ |hash, key| hash[key] = [] }
|
|
129
|
+
|
|
130
|
+
# There's no need to call `synchronize` here. We're opening a second handle to
|
|
131
|
+
# the file, and `flock`ing it, which prevents inconsistent reads
|
|
132
|
+
stores_for_metric.each do |file_path|
|
|
133
|
+
begin
|
|
134
|
+
store = FileMappedDict.new(file_path, true)
|
|
135
|
+
store.all_values.each do |(labelset_qs, v, ts)|
|
|
136
|
+
# Labels come as a query string, and CGI::parse returns arrays for each key
|
|
137
|
+
# "foo=bar&x=y" => { "foo" => ["bar"], "x" => ["y"] }
|
|
138
|
+
# Turn the keys back into symbols, and remove the arrays
|
|
139
|
+
label_set = CGI::parse(labelset_qs).map do |k, vs|
|
|
140
|
+
[k.to_sym, vs.first]
|
|
141
|
+
end.to_h
|
|
142
|
+
|
|
143
|
+
stores_data[label_set] << [v, ts]
|
|
144
|
+
end
|
|
145
|
+
ensure
|
|
146
|
+
store.close if store
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Aggregate all the different values for each label_set
|
|
151
|
+
aggregate_hash = Hash.new { |hash, key| hash[key] = 0.0 }
|
|
152
|
+
stores_data.each_with_object(aggregate_hash) do |(label_set, values), acc|
|
|
153
|
+
acc[label_set] = aggregate_values(values)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def in_process_sync
|
|
160
|
+
@lock.synchronize { yield }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def store_key(labels)
|
|
164
|
+
if @values_aggregation_mode == ALL
|
|
165
|
+
labels[:pid] = process_id
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
labels.to_a.sort.map{|k,v| "#{CGI::escape(k.to_s)}=#{CGI::escape(v.to_s)}"}.join('&')
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def internal_store
|
|
172
|
+
if @store_opened_by_pid != process_id
|
|
173
|
+
@store_opened_by_pid = process_id
|
|
174
|
+
@internal_store = FileMappedDict.new(filemap_filename)
|
|
175
|
+
else
|
|
176
|
+
@internal_store
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Filename for this metric's PStore (one per process)
|
|
181
|
+
def filemap_filename
|
|
182
|
+
filename = "metric_#{ metric_name }___#{ process_id }.bin"
|
|
183
|
+
File.join(@store_settings[:dir], filename)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def stores_for_metric
|
|
187
|
+
Dir.glob(File.join(@store_settings[:dir], "metric_#{ metric_name }___*"))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def process_id
|
|
191
|
+
Process.pid
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def aggregate_values(values)
|
|
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
|
|
201
|
+
else
|
|
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
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
private_constant :MetricStore
|
|
221
|
+
|
|
222
|
+
# A dict of doubles, backed by an file we access directly as a byte array.
|
|
223
|
+
#
|
|
224
|
+
# The file starts with a 4 byte int, indicating how much of it is used.
|
|
225
|
+
# Then 4 bytes of padding.
|
|
226
|
+
# There's then a number of entries, consisting of a 4 byte int which is the
|
|
227
|
+
# size of the next field, a utf-8 encoded string key, padding to an 8 byte
|
|
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.
|
|
230
|
+
class FileMappedDict
|
|
231
|
+
INITIAL_FILE_SIZE = 1024*1024
|
|
232
|
+
|
|
233
|
+
attr_reader :capacity, :used, :positions
|
|
234
|
+
|
|
235
|
+
def initialize(filename, readonly = false)
|
|
236
|
+
@positions = {}
|
|
237
|
+
@used = 0
|
|
238
|
+
|
|
239
|
+
open_file(filename, readonly)
|
|
240
|
+
@used = @f.read(4).unpack('l')[0] if @capacity > 0
|
|
241
|
+
|
|
242
|
+
if @used > 0
|
|
243
|
+
# File already has data. Read the existing values
|
|
244
|
+
with_file_lock { populate_positions }
|
|
245
|
+
else
|
|
246
|
+
# File is empty. Init the `used` counter, if we're in write mode
|
|
247
|
+
if !readonly
|
|
248
|
+
@used = 8
|
|
249
|
+
@f.seek(0)
|
|
250
|
+
@f.write([@used].pack('l'))
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Return a list of key-value pairs
|
|
256
|
+
def all_values
|
|
257
|
+
with_file_lock do
|
|
258
|
+
@positions.map do |key, pos|
|
|
259
|
+
@f.seek(pos)
|
|
260
|
+
value, timestamp = @f.read(16).unpack('dd')
|
|
261
|
+
[key, value, timestamp]
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def read_value(key)
|
|
267
|
+
if !@positions.has_key?(key)
|
|
268
|
+
init_value(key)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
pos = @positions[key]
|
|
272
|
+
@f.seek(pos)
|
|
273
|
+
@f.read(8).unpack('d')[0]
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def write_value(key, value)
|
|
277
|
+
if !@positions.has_key?(key)
|
|
278
|
+
init_value(key)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
282
|
+
pos = @positions[key]
|
|
283
|
+
@f.seek(pos)
|
|
284
|
+
@f.write([value, now].pack('dd'))
|
|
285
|
+
@f.flush
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def increment_value(key, by)
|
|
289
|
+
if !@positions.has_key?(key)
|
|
290
|
+
init_value(key)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
pos = @positions[key]
|
|
294
|
+
@f.seek(pos)
|
|
295
|
+
value = @f.read(8).unpack('d')[0]
|
|
296
|
+
|
|
297
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
298
|
+
@f.seek(-8, :CUR)
|
|
299
|
+
@f.write([value + by, now].pack('dd'))
|
|
300
|
+
@f.flush
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def close
|
|
304
|
+
@f.close
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def with_file_lock
|
|
308
|
+
@f.flock(File::LOCK_EX)
|
|
309
|
+
yield
|
|
310
|
+
ensure
|
|
311
|
+
@f.flock(File::LOCK_UN)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
private
|
|
315
|
+
|
|
316
|
+
def open_file(filename, readonly)
|
|
317
|
+
mode = if readonly
|
|
318
|
+
"r"
|
|
319
|
+
elsif File.exist?(filename)
|
|
320
|
+
"r+b"
|
|
321
|
+
else
|
|
322
|
+
"w+b"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
@f = File.open(filename, mode)
|
|
326
|
+
if @f.size == 0 && !readonly
|
|
327
|
+
resize_file(INITIAL_FILE_SIZE)
|
|
328
|
+
end
|
|
329
|
+
@capacity = @f.size
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def resize_file(new_capacity)
|
|
333
|
+
@f.truncate(new_capacity)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Initialize a value. Lock must be held by caller.
|
|
337
|
+
def init_value(key)
|
|
338
|
+
# Pad to be 8-byte aligned.
|
|
339
|
+
padded = key + (' ' * (8 - (key.length + 4) % 8))
|
|
340
|
+
value = [padded.length, padded, 0.0, 0.0].pack("lA#{padded.length}dd")
|
|
341
|
+
while @used + value.length > @capacity
|
|
342
|
+
@capacity *= 2
|
|
343
|
+
resize_file(@capacity)
|
|
344
|
+
end
|
|
345
|
+
@f.seek(@used)
|
|
346
|
+
@f.write(value)
|
|
347
|
+
@used += value.length
|
|
348
|
+
@f.seek(0)
|
|
349
|
+
@f.write([@used].pack('l'))
|
|
350
|
+
@f.flush
|
|
351
|
+
@positions[key] = @used - 16
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Read position of all keys. No locking is performed.
|
|
355
|
+
def populate_positions
|
|
356
|
+
@f.seek(8)
|
|
357
|
+
while @f.pos < @used
|
|
358
|
+
padded_len = @f.read(4).unpack('l')[0]
|
|
359
|
+
key = @f.read(padded_len).unpack("A#{padded_len}")[0].strip
|
|
360
|
+
@positions[key] = @f.pos
|
|
361
|
+
@f.seek(16, :CUR)
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|