prometheus-client 0.11.0.pre.alpha.1 → 3.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 +4 -4
- data/README.md +147 -42
- data/lib/prometheus/client/data_stores/README.md +1 -1
- data/lib/prometheus/client/data_stores/direct_file_store.rb +55 -27
- data/lib/prometheus/client/histogram.rb +41 -11
- data/lib/prometheus/client/label_set_validator.rb +9 -2
- data/lib/prometheus/client/metric.rb +30 -10
- data/lib/prometheus/client/push.rb +126 -12
- data/lib/prometheus/client/registry.rb +4 -4
- data/lib/prometheus/client/summary.rb +17 -3
- data/lib/prometheus/client/version.rb +1 -1
- data/lib/prometheus/middleware/collector.rb +54 -4
- data/lib/prometheus/middleware/exporter.rb +6 -1
- metadata +10 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b7017e1e3c284f9558d758e91d4855ad97b5df63fdef115cee035aa0948aed30
|
4
|
+
data.tar.gz: 52a7abaadf1addf7bf9303d73a31a14338429882b488eb80c2144a6817cfdb7d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1e3b2ff9368dacbdc175ea8cb709e1ad7be73b0d8e644468f65bf36f44b517798a28d7f5dab73b538519fc1a918f977e083e101986a761fdf2fe42ab719fcc1d
|
7
|
+
data.tar.gz: 7fb5a1887a88f7687f2af5ef4839e26030da25cc0c51a03445bbb43706caf8d2231c65108cf3265cc024bc286d5dc8a38ec5d29145b39b2f0cbd0277f29f1972
|
data/README.md
CHANGED
@@ -5,11 +5,17 @@ through a HTTP interface. Intended to be used together with a
|
|
5
5
|
[Prometheus server][1].
|
6
6
|
|
7
7
|
[![Gem Version][4]](http://badge.fury.io/rb/prometheus-client)
|
8
|
-
[![Build Status][3]](
|
9
|
-
[![Coverage Status][7]](https://coveralls.io/r/prometheus/client_ruby)
|
8
|
+
[![Build Status][3]](https://circleci.com/gh/prometheus/client_ruby/tree/master.svg?style=svg)
|
10
9
|
|
11
10
|
## Usage
|
12
11
|
|
12
|
+
### Installation
|
13
|
+
|
14
|
+
For a global installation run `gem install prometheus-client`.
|
15
|
+
|
16
|
+
If you're using [Bundler](https://bundler.io/) add `gem "prometheus-client"` to your `Gemfile`.
|
17
|
+
Make sure to run `bundle install` afterwards.
|
18
|
+
|
13
19
|
### Overview
|
14
20
|
|
15
21
|
```ruby
|
@@ -64,7 +70,7 @@ integrated [example application](examples/rack/README.md).
|
|
64
70
|
The Ruby client can also be used to push its collected metrics to a
|
65
71
|
[Pushgateway][8]. This comes in handy with batch jobs or in other scenarios
|
66
72
|
where it's not possible or feasible to let a Prometheus server scrape a Ruby
|
67
|
-
process. TLS and basic
|
73
|
+
process. TLS and HTTP basic authentication are supported.
|
68
74
|
|
69
75
|
```ruby
|
70
76
|
require 'prometheus/client'
|
@@ -74,18 +80,59 @@ registry = Prometheus::Client.registry
|
|
74
80
|
# ... register some metrics, set/increment/observe/etc. their values
|
75
81
|
|
76
82
|
# push the registry state to the default gateway
|
77
|
-
Prometheus::Client::Push.new('my-batch-job').add(registry)
|
83
|
+
Prometheus::Client::Push.new(job: 'my-batch-job').add(registry)
|
84
|
+
|
85
|
+
# optional: specify a grouping key that uniquely identifies a job instance, and gateway.
|
86
|
+
#
|
87
|
+
# Note: the labels you use in the grouping key must not conflict with labels set on the
|
88
|
+
# metrics being pushed. If they do, an error will be raised.
|
89
|
+
Prometheus::Client::Push.new(
|
90
|
+
job: 'my-batch-job',
|
91
|
+
gateway: 'https://example.domain:1234',
|
92
|
+
grouping_key: { instance: 'some-instance', extra_key: 'foobar' }
|
93
|
+
).add(registry)
|
94
|
+
|
95
|
+
# If you want to replace any previously pushed metrics for a given grouping key,
|
96
|
+
# use the #replace method.
|
97
|
+
#
|
98
|
+
# Unlike #add, this will completely replace the metrics under the specified grouping key
|
99
|
+
# (i.e. anything currently present in the pushgateway for the specified grouping key, but
|
100
|
+
# not present in the registry for that grouping key will be removed).
|
101
|
+
#
|
102
|
+
# See https://github.com/prometheus/pushgateway#put-method for a full explanation.
|
103
|
+
Prometheus::Client::Push.new(job: 'my-batch-job').replace(registry)
|
104
|
+
|
105
|
+
# If you want to delete all previously pushed metrics for a given grouping key,
|
106
|
+
# use the #delete method.
|
107
|
+
Prometheus::Client::Push.new(job: 'my-batch-job').delete
|
108
|
+
```
|
78
109
|
|
79
|
-
|
80
|
-
Prometheus::Client::Push.new('my-batch-job', 'foobar', 'https://example.domain:1234').add(registry)
|
110
|
+
#### Basic authentication
|
81
111
|
|
82
|
-
|
83
|
-
|
84
|
-
|
112
|
+
By design, `Prometheus::Client::Push` doesn't read credentials for HTTP basic
|
113
|
+
authentication when they are passed in via the gateway URL using the
|
114
|
+
`http://user:password@example.com:9091` syntax, and will in fact raise an error if they're
|
115
|
+
supplied that way.
|
85
116
|
|
86
|
-
|
87
|
-
|
88
|
-
|
117
|
+
The reason for this is that when using that syntax, the username and password
|
118
|
+
have to follow the usual rules for URL encoding of characters [per RFC
|
119
|
+
3986](https://datatracker.ietf.org/doc/html/rfc3986#section-2.1).
|
120
|
+
|
121
|
+
Rather than place the burden of correctly performing that encoding on users of this gem,
|
122
|
+
we decided to have a separate method for supplying HTTP basic authentication credentials,
|
123
|
+
with no requirement to URL encode the characters in them.
|
124
|
+
|
125
|
+
Instead of passing credentials like this:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
push = Prometheus::Client::Push.new(job: "my-job", gateway: "http://user:password@localhost:9091")
|
129
|
+
```
|
130
|
+
|
131
|
+
please pass them like this:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
push = Prometheus::Client::Push.new(job: "my-job", gateway: "http://localhost:9091")
|
135
|
+
push.basic_auth("user", "password")
|
89
136
|
```
|
90
137
|
|
91
138
|
## Metrics
|
@@ -151,6 +198,11 @@ histogram.get(labels: { service: 'users' })
|
|
151
198
|
# => { 0.005 => 3, 0.01 => 15, 0.025 => 18, ..., 2.5 => 42, 5 => 42, 10 = >42 }
|
152
199
|
```
|
153
200
|
|
201
|
+
Histograms provide default buckets of `[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]`
|
202
|
+
|
203
|
+
You can specify your own buckets, either explicitly, or using the `Histogram.linear_buckets`
|
204
|
+
or `Histogram.exponential_buckets` methods to define regularly spaced buckets.
|
205
|
+
|
154
206
|
### Summary
|
155
207
|
|
156
208
|
Summary, similar to histograms, is an accumulator for samples. It captures
|
@@ -254,6 +306,12 @@ class MyComponent
|
|
254
306
|
end
|
255
307
|
```
|
256
308
|
|
309
|
+
### `init_label_set`
|
310
|
+
|
311
|
+
The time series of a metric are not initialized until something happens. For counters, for example, this means that the time series do not exist until the counter is incremented for the first time.
|
312
|
+
|
313
|
+
To get around this problem the client provides the `init_label_set` method that can be used to initialise the time series of a metric for a given label set.
|
314
|
+
|
257
315
|
### Reserved labels
|
258
316
|
|
259
317
|
The following labels are reserved by the client library, and attempting to use them in a
|
@@ -271,7 +329,7 @@ is stored in a global Data Store object, rather than in the metric objects thems
|
|
271
329
|
(This "storage" is ephemeral, generally in-memory, it's not "long-term storage")
|
272
330
|
|
273
331
|
The main reason to do this is that different applications may have different requirements
|
274
|
-
for their metrics storage.
|
332
|
+
for their metrics storage. Applications running in pre-fork servers (like Unicorn, for
|
275
333
|
example), require a shared store between all the processes, to be able to report coherent
|
276
334
|
numbers. At the same time, other applications may not have this requirement but be very
|
277
335
|
sensitive to performance, and would prefer instead a simpler, faster store.
|
@@ -307,11 +365,11 @@ When instantiating metrics, there is an optional `store_settings` attribute. Thi
|
|
307
365
|
to set up store-specific settings for each metric. For most stores, this is not used, but
|
308
366
|
for multi-process stores, this is used to specify how to aggregate the values of each
|
309
367
|
metric across multiple processes. For the most part, this is used for Gauges, to specify
|
310
|
-
whether you want to report the `SUM`, `MAX` or `
|
311
|
-
For almost all other cases, you'd leave the default (`SUM`). More on this
|
312
|
-
*Aggregation* section below.
|
368
|
+
whether you want to report the `SUM`, `MAX`, `MIN`, or `MOST_RECENT` value observed across
|
369
|
+
all processes. For almost all other cases, you'd leave the default (`SUM`). More on this
|
370
|
+
on the *Aggregation* section below.
|
313
371
|
|
314
|
-
|
372
|
+
Custom stores may also accept extra parameters besides `:aggregation`. See the
|
315
373
|
documentation of each store for more details.
|
316
374
|
|
317
375
|
### Built-in stores
|
@@ -326,26 +384,73 @@ There are 3 built-in stores, with different trade-offs:
|
|
326
384
|
it's absolutely not thread safe.
|
327
385
|
- **DirectFileStore**: Stores data in binary files, one file per process and per metric.
|
328
386
|
This is generally the recommended store to use with pre-fork servers and other
|
329
|
-
"multi-process" scenarios.
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
387
|
+
"multi-process" scenarios. There are some important caveats to using this store, so
|
388
|
+
please read on the section below.
|
389
|
+
|
390
|
+
### `DirectFileStore` caveats and things to keep in mind
|
391
|
+
|
392
|
+
Each metric gets a file for each process, and manages its contents by storing keys and
|
393
|
+
binary floats next to them, and updating the offsets of those Floats directly. When
|
394
|
+
exporting metrics, it will find all the files that apply to each metric, read them,
|
395
|
+
and aggregate them.
|
396
|
+
|
397
|
+
**Aggregation of metrics**: Since there will be several files per metrics (one per process),
|
398
|
+
these need to be aggregated to present a coherent view to Prometheus. Depending on your
|
399
|
+
use case, you may need to control how this works. When using this store,
|
400
|
+
each Metric allows you to specify an `:aggregation` setting, defining how
|
401
|
+
to aggregate the multiple possible values we can get for each labelset. By default,
|
402
|
+
Counters, Histograms and Summaries are `SUM`med, and Gauges report all their values (one
|
403
|
+
for each process), tagged with a `pid` label. You can also select `SUM`, `MAX`, `MIN`, or
|
404
|
+
`MOST_RECENT` for your gauges, depending on your use case.
|
405
|
+
|
406
|
+
Please note that that the `MOST_RECENT` aggregation only works for gauges, and it does not
|
407
|
+
allow the use of `increment` / `decrement`, you can only use `set`.
|
408
|
+
|
409
|
+
**Memory Usage**: When scraped by Prometheus, this store will read all these files, get all
|
410
|
+
the values and aggregate them. We have notice this can have a noticeable effect on memory
|
411
|
+
usage for your app. We recommend you test this in a realistic usage scenario to make sure
|
412
|
+
you won't hit any memory limits your app may have.
|
413
|
+
|
414
|
+
**Resetting your metrics on each run**: You should also make sure that the directory where
|
415
|
+
you store your metric files (specified when initializing the `DirectFileStore`) is emptied
|
416
|
+
when your app starts. Otherwise, each app run will continue exporting the metrics from the
|
417
|
+
previous run.
|
418
|
+
|
419
|
+
If you have this issue, one way to do this is to run code similar to this as part of you
|
420
|
+
initialization:
|
421
|
+
|
422
|
+
```ruby
|
423
|
+
Dir["#{app_path}/tmp/prometheus/*.bin"].each do |file_path|
|
424
|
+
File.unlink(file_path)
|
425
|
+
end
|
426
|
+
```
|
427
|
+
|
428
|
+
If you are running in pre-fork servers (such as Unicorn or Puma with multiple processes),
|
429
|
+
make sure you do this **before** the server forks. Otherwise, each child process may delete
|
430
|
+
files created by other processes on *this* run, instead of deleting old files.
|
431
|
+
|
432
|
+
**Declare metrics before fork**: As well as deleting files before your process forks, you
|
433
|
+
should make sure to declare your metrics before forking too. Because the metric registry
|
434
|
+
is held in memory, any metrics declared after forking will only be present in child
|
435
|
+
processes where the code declaring them ran, and as a result may not be consistently
|
436
|
+
exported when scraped (i.e. they will only appear when a child process that declared them
|
437
|
+
is scraped).
|
438
|
+
|
439
|
+
If you're absolutely sure that every child process will run the metric declaration code,
|
440
|
+
then you won't run into this issue, but the simplest approach is to declare the metrics
|
441
|
+
before forking.
|
442
|
+
|
443
|
+
**Large numbers of files**: Because there is an individual file per metric and per process
|
444
|
+
(which is done to optimize for observation performance), you may end up with a large number
|
445
|
+
of files. We don't currently have a solution for this problem, but we're working on it.
|
446
|
+
|
447
|
+
**Performance**: Even though this store saves data on disk, it's still much faster than
|
448
|
+
would probably be expected, because the files are never actually `fsync`ed, so the store
|
449
|
+
never blocks while waiting for disk. The kernel's page cache is incredibly efficient in
|
450
|
+
this regard. If in doubt, check the benchmark scripts described in the documentation for
|
451
|
+
creating your own stores and run them in your particular runtime environment to make sure
|
452
|
+
this provides adequate performance.
|
453
|
+
|
349
454
|
|
350
455
|
### Building your own store, and stores other than the built-in ones.
|
351
456
|
|
@@ -364,16 +469,16 @@ If you are in a multi-process environment (such as pre-fork servers like Unicorn
|
|
364
469
|
process will probably keep their own counters, which need to be aggregated when receiving
|
365
470
|
a Prometheus scrape, to report coherent total numbers.
|
366
471
|
|
367
|
-
For Counters
|
472
|
+
For Counters, Histograms and quantile-less Summaries this is simply a matter of
|
368
473
|
summing the values of each process.
|
369
474
|
|
370
475
|
For Gauges, however, this may not be the right thing to do, depending on what they're
|
371
476
|
measuring. You might want to take the maximum or minimum value observed in any process,
|
372
|
-
rather than the sum of all of them.
|
373
|
-
value.
|
477
|
+
rather than the sum of all of them. By default, we export each process's individual
|
478
|
+
value, with a `pid` label identifying each one.
|
374
479
|
|
375
|
-
|
376
|
-
metric, to specify an `:aggregation` setting.
|
480
|
+
If these defaults don't work for your use case, you should use the `store_settings`
|
481
|
+
parameter when registering the metric, to specify an `:aggregation` setting.
|
377
482
|
|
378
483
|
```ruby
|
379
484
|
free_disk_space = registry.gauge(:free_disk_space_bytes,
|
@@ -187,7 +187,7 @@ has created a good amount of research, benchmarks, and experimental stores, whic
|
|
187
187
|
weren't useful to include in this repo, but may be a useful resource or starting point
|
188
188
|
if you are building your own store.
|
189
189
|
|
190
|
-
Check out the [GoCardless Data Stores Experiments](gocardless/prometheus-client-ruby-data-stores-experiments)
|
190
|
+
Check out the [GoCardless Data Stores Experiments](https://github.com/gocardless/prometheus-client-ruby-data-stores-experiments)
|
191
191
|
repository for these.
|
192
192
|
|
193
193
|
## Sample, imaginary multi-process Data Store
|
@@ -18,14 +18,18 @@ module Prometheus
|
|
18
18
|
#
|
19
19
|
# In order to do this, each Metric needs an `:aggregation` setting, specifying how
|
20
20
|
# to aggregate the multiple possible values we can get for each labelset. By default,
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
# the highest value of all the processes / threads.
|
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.
|
25
29
|
|
26
30
|
class DirectFileStore
|
27
31
|
class InvalidStoreSettingsError < StandardError; end
|
28
|
-
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]
|
29
33
|
DEFAULT_METRIC_SETTINGS = { aggregation: SUM }
|
30
34
|
DEFAULT_GAUGE_SETTINGS = { aggregation: ALL }
|
31
35
|
|
@@ -41,7 +45,7 @@ module Prometheus
|
|
41
45
|
end
|
42
46
|
|
43
47
|
settings = default_settings.merge(metric_settings)
|
44
|
-
validate_metric_settings(settings)
|
48
|
+
validate_metric_settings(metric_type, settings)
|
45
49
|
|
46
50
|
MetricStore.new(metric_name: metric_name,
|
47
51
|
store_settings: @store_settings,
|
@@ -50,7 +54,7 @@ module Prometheus
|
|
50
54
|
|
51
55
|
private
|
52
56
|
|
53
|
-
def validate_metric_settings(metric_settings)
|
57
|
+
def validate_metric_settings(metric_type, metric_settings)
|
54
58
|
unless metric_settings.has_key?(:aggregation) &&
|
55
59
|
AGGREGATION_MODES.include?(metric_settings[:aggregation])
|
56
60
|
raise InvalidStoreSettingsError,
|
@@ -61,6 +65,11 @@ module Prometheus
|
|
61
65
|
raise InvalidStoreSettingsError,
|
62
66
|
"Only :aggregation setting can be specified"
|
63
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
|
64
73
|
end
|
65
74
|
|
66
75
|
class MetricStore
|
@@ -70,6 +79,7 @@ module Prometheus
|
|
70
79
|
@metric_name = metric_name
|
71
80
|
@store_settings = store_settings
|
72
81
|
@values_aggregation_mode = metric_settings[:aggregation]
|
82
|
+
@store_opened_by_pid = nil
|
73
83
|
|
74
84
|
@lock = Monitor.new
|
75
85
|
end
|
@@ -96,6 +106,12 @@ module Prometheus
|
|
96
106
|
end
|
97
107
|
|
98
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
|
+
|
99
115
|
key = store_key(labels)
|
100
116
|
in_process_sync do
|
101
117
|
value = internal_store.read_value(key)
|
@@ -117,7 +133,7 @@ module Prometheus
|
|
117
133
|
stores_for_metric.each do |file_path|
|
118
134
|
begin
|
119
135
|
store = FileMappedDict.new(file_path, true)
|
120
|
-
store.all_values.each do |(labelset_qs, v)|
|
136
|
+
store.all_values.each do |(labelset_qs, v, ts)|
|
121
137
|
# Labels come as a query string, and CGI::parse returns arrays for each key
|
122
138
|
# "foo=bar&x=y" => { "foo" => ["bar"], "x" => ["y"] }
|
123
139
|
# Turn the keys back into symbols, and remove the arrays
|
@@ -125,7 +141,7 @@ module Prometheus
|
|
125
141
|
[k.to_sym, vs.first]
|
126
142
|
end.to_h
|
127
143
|
|
128
|
-
stores_data[label_set] << v
|
144
|
+
stores_data[label_set] << [v, ts]
|
129
145
|
end
|
130
146
|
ensure
|
131
147
|
store.close if store
|
@@ -177,30 +193,41 @@ module Prometheus
|
|
177
193
|
end
|
178
194
|
|
179
195
|
def aggregate_values(values)
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
elsif @values_aggregation_mode == ALL
|
187
|
-
values.first
|
196
|
+
# Each entry in the `values` array is a tuple of `value` and `timestamp`,
|
197
|
+
# so for all aggregations except `MOST_RECENT`, we need to only take the
|
198
|
+
# first value in each entry and ignore the second.
|
199
|
+
if @values_aggregation_mode == MOST_RECENT
|
200
|
+
latest_tuple = values.max { |a,b| a[1] <=> b[1] }
|
201
|
+
latest_tuple.first # return the value without the timestamp
|
188
202
|
else
|
189
|
-
|
190
|
-
|
203
|
+
values = values.map(&:first) # Discard timestamps
|
204
|
+
|
205
|
+
if @values_aggregation_mode == SUM
|
206
|
+
values.inject { |sum, element| sum + element }
|
207
|
+
elsif @values_aggregation_mode == MAX
|
208
|
+
values.max
|
209
|
+
elsif @values_aggregation_mode == MIN
|
210
|
+
values.min
|
211
|
+
elsif @values_aggregation_mode == ALL
|
212
|
+
values.first
|
213
|
+
else
|
214
|
+
raise InvalidStoreSettingsError,
|
215
|
+
"Invalid Aggregation Mode: #{ @values_aggregation_mode }"
|
216
|
+
end
|
191
217
|
end
|
192
218
|
end
|
193
219
|
end
|
194
220
|
|
195
221
|
private_constant :MetricStore
|
196
222
|
|
197
|
-
# A dict of doubles, backed by an file we access directly
|
223
|
+
# A dict of doubles, backed by an file we access directly as a byte array.
|
198
224
|
#
|
199
225
|
# The file starts with a 4 byte int, indicating how much of it is used.
|
200
226
|
# Then 4 bytes of padding.
|
201
227
|
# There's then a number of entries, consisting of a 4 byte int which is the
|
202
228
|
# size of the next field, a utf-8 encoded string key, padding to an 8 byte
|
203
|
-
# alignment, and then a 8 byte float which is the value
|
229
|
+
# alignment, and then a 8 byte float which is the value, and then a 8 byte
|
230
|
+
# float which is the unix timestamp when the value was set.
|
204
231
|
class FileMappedDict
|
205
232
|
INITIAL_FILE_SIZE = 1024*1024
|
206
233
|
|
@@ -231,8 +258,8 @@ module Prometheus
|
|
231
258
|
with_file_lock do
|
232
259
|
@positions.map do |key, pos|
|
233
260
|
@f.seek(pos)
|
234
|
-
value = @f.read(
|
235
|
-
[key, value]
|
261
|
+
value, timestamp = @f.read(16).unpack('dd')
|
262
|
+
[key, value, timestamp]
|
236
263
|
end
|
237
264
|
end
|
238
265
|
end
|
@@ -252,9 +279,10 @@ module Prometheus
|
|
252
279
|
init_value(key)
|
253
280
|
end
|
254
281
|
|
282
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
255
283
|
pos = @positions[key]
|
256
284
|
@f.seek(pos)
|
257
|
-
@f.write([value].pack('
|
285
|
+
@f.write([value, now].pack('dd'))
|
258
286
|
@f.flush
|
259
287
|
end
|
260
288
|
|
@@ -295,7 +323,7 @@ module Prometheus
|
|
295
323
|
def init_value(key)
|
296
324
|
# Pad to be 8-byte aligned.
|
297
325
|
padded = key + (' ' * (8 - (key.length + 4) % 8))
|
298
|
-
value = [padded.length, padded, 0.0].pack("lA#{padded.length}
|
326
|
+
value = [padded.length, padded, 0.0, 0.0].pack("lA#{padded.length}dd")
|
299
327
|
while @used + value.length > @capacity
|
300
328
|
@capacity *= 2
|
301
329
|
resize_file(@capacity)
|
@@ -306,7 +334,7 @@ module Prometheus
|
|
306
334
|
@f.seek(0)
|
307
335
|
@f.write([@used].pack('l'))
|
308
336
|
@f.flush
|
309
|
-
@positions[key] = @used -
|
337
|
+
@positions[key] = @used - 16
|
310
338
|
end
|
311
339
|
|
312
340
|
# Read position of all keys. No locking is performed.
|
@@ -316,7 +344,7 @@ module Prometheus
|
|
316
344
|
padded_len = @f.read(4).unpack('l')[0]
|
317
345
|
key = @f.read(padded_len).unpack("A#{padded_len}")[0].strip
|
318
346
|
@positions[key] = @f.pos
|
319
|
-
@f.seek(
|
347
|
+
@f.seek(16, :CUR)
|
320
348
|
end
|
321
349
|
end
|
322
350
|
end
|
@@ -6,7 +6,7 @@ module Prometheus
|
|
6
6
|
module Client
|
7
7
|
# A histogram samples observations (usually things like request durations
|
8
8
|
# or response sizes) and counts them in configurable buckets. It also
|
9
|
-
# provides a sum of all observed values.
|
9
|
+
# provides a total count and sum of all observed values.
|
10
10
|
class Histogram < Metric
|
11
11
|
# DEFAULT_BUCKETS are the default Histogram buckets. The default buckets
|
12
12
|
# are tailored to broadly measure the response time (in seconds) of a
|
@@ -33,21 +33,41 @@ module Prometheus
|
|
33
33
|
store_settings: store_settings)
|
34
34
|
end
|
35
35
|
|
36
|
+
def self.linear_buckets(start:, width:, count:)
|
37
|
+
count.times.map { |idx| start.to_f + idx * width }
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.exponential_buckets(start:, factor: 2, count:)
|
41
|
+
count.times.map { |idx| start.to_f * factor ** idx }
|
42
|
+
end
|
43
|
+
|
36
44
|
def with_labels(labels)
|
37
|
-
self.class.new(name,
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
45
|
+
new_metric = self.class.new(name,
|
46
|
+
docstring: docstring,
|
47
|
+
labels: @labels,
|
48
|
+
preset_labels: preset_labels.merge(labels),
|
49
|
+
buckets: @buckets,
|
50
|
+
store_settings: @store_settings)
|
51
|
+
|
52
|
+
# The new metric needs to use the same store as the "main" declared one, otherwise
|
53
|
+
# any observations on that copy with the pre-set labels won't actually be exported.
|
54
|
+
new_metric.replace_internal_store(@store)
|
55
|
+
|
56
|
+
new_metric
|
43
57
|
end
|
44
58
|
|
45
59
|
def type
|
46
60
|
:histogram
|
47
61
|
end
|
48
62
|
|
63
|
+
# Records a given value. The recorded value is usually positive
|
64
|
+
# or zero. A negative value is accepted but prevents current
|
65
|
+
# versions of Prometheus from properly detecting counter resets
|
66
|
+
# in the sum of observations. See
|
67
|
+
# https://prometheus.io/docs/practices/histograms/#count-and-sum-of-observations
|
68
|
+
# for details.
|
49
69
|
def observe(value, labels: {})
|
50
|
-
bucket = buckets.find {|upper_limit| upper_limit
|
70
|
+
bucket = buckets.find {|upper_limit| upper_limit >= value }
|
51
71
|
bucket = "+Inf" if bucket.nil?
|
52
72
|
|
53
73
|
base_label_set = label_set_for(labels)
|
@@ -81,19 +101,29 @@ module Prometheus
|
|
81
101
|
|
82
102
|
# Returns all label sets with their values expressed as hashes with their buckets
|
83
103
|
def values
|
84
|
-
|
104
|
+
values = @store.all_values
|
85
105
|
|
86
|
-
result =
|
106
|
+
result = values.each_with_object({}) do |(label_set, v), acc|
|
87
107
|
actual_label_set = label_set.reject{|l| l == :le }
|
88
108
|
acc[actual_label_set] ||= @buckets.map{|b| [b.to_s, 0.0]}.to_h
|
89
109
|
acc[actual_label_set][label_set[:le].to_s] = v
|
90
110
|
end
|
91
111
|
|
92
|
-
result.each do |(
|
112
|
+
result.each do |(_label_set, v)|
|
93
113
|
accumulate_buckets(v)
|
94
114
|
end
|
95
115
|
end
|
96
116
|
|
117
|
+
def init_label_set(labels)
|
118
|
+
base_label_set = label_set_for(labels)
|
119
|
+
|
120
|
+
@store.synchronize do
|
121
|
+
(buckets + ["+Inf", "sum"]).each do |bucket|
|
122
|
+
@store.set(labels: base_label_set.merge(le: bucket.to_s), val: 0)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
97
127
|
private
|
98
128
|
|
99
129
|
# Modifies the passed in parameter
|
@@ -7,6 +7,7 @@ module Prometheus
|
|
7
7
|
class LabelSetValidator
|
8
8
|
# TODO: we might allow setting :instance in the future
|
9
9
|
BASE_RESERVED_LABELS = [:job, :instance, :pid].freeze
|
10
|
+
LABEL_NAME_REGEX = /\A[a-zA-Z_][a-zA-Z0-9_]*\Z/
|
10
11
|
|
11
12
|
class LabelSetError < StandardError; end
|
12
13
|
class InvalidLabelSetError < LabelSetError; end
|
@@ -59,9 +60,15 @@ module Prometheus
|
|
59
60
|
end
|
60
61
|
|
61
62
|
def validate_name(key)
|
62
|
-
|
63
|
+
if key.to_s.start_with?('__')
|
64
|
+
raise ReservedLabelError, "label #{key} must not start with __"
|
65
|
+
end
|
66
|
+
|
67
|
+
unless key.to_s =~ LABEL_NAME_REGEX
|
68
|
+
raise InvalidLabelError, "label name must match /#{LABEL_NAME_REGEX}/"
|
69
|
+
end
|
63
70
|
|
64
|
-
|
71
|
+
true
|
65
72
|
end
|
66
73
|
|
67
74
|
def validate_reserved_key(key)
|
@@ -7,7 +7,7 @@ module Prometheus
|
|
7
7
|
module Client
|
8
8
|
# Metric
|
9
9
|
class Metric
|
10
|
-
attr_reader :name, :docstring, :preset_labels
|
10
|
+
attr_reader :name, :docstring, :labels, :preset_labels
|
11
11
|
|
12
12
|
def initialize(name,
|
13
13
|
docstring:,
|
@@ -29,18 +29,28 @@ module Prometheus
|
|
29
29
|
@docstring = docstring
|
30
30
|
@preset_labels = stringify_values(preset_labels)
|
31
31
|
|
32
|
+
@all_labels_preset = false
|
33
|
+
if preset_labels.keys.length == labels.length
|
34
|
+
@validator.validate_labelset!(preset_labels)
|
35
|
+
@all_labels_preset = true
|
36
|
+
end
|
37
|
+
|
32
38
|
@store = Prometheus::Client.config.data_store.for_metric(
|
33
39
|
name,
|
34
40
|
metric_type: type,
|
35
41
|
metric_settings: store_settings
|
36
42
|
)
|
37
43
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
44
|
+
# WARNING: Our internal store can be replaced later by `with_labels`
|
45
|
+
# Everything we do after this point needs to still work if @store gets replaced
|
46
|
+
init_label_set({}) if labels.empty?
|
47
|
+
end
|
48
|
+
|
49
|
+
protected def replace_internal_store(new_store)
|
50
|
+
@store = new_store
|
42
51
|
end
|
43
52
|
|
53
|
+
|
44
54
|
# Returns the value for the given label set
|
45
55
|
def get(labels: {})
|
46
56
|
label_set = label_set_for(labels)
|
@@ -48,11 +58,21 @@ module Prometheus
|
|
48
58
|
end
|
49
59
|
|
50
60
|
def with_labels(labels)
|
51
|
-
self.class.new(name,
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
61
|
+
new_metric = self.class.new(name,
|
62
|
+
docstring: docstring,
|
63
|
+
labels: @labels,
|
64
|
+
preset_labels: preset_labels.merge(labels),
|
65
|
+
store_settings: @store_settings)
|
66
|
+
|
67
|
+
# The new metric needs to use the same store as the "main" declared one, otherwise
|
68
|
+
# any observations on that copy with the pre-set labels won't actually be exported.
|
69
|
+
new_metric.replace_internal_store(@store)
|
70
|
+
|
71
|
+
new_metric
|
72
|
+
end
|
73
|
+
|
74
|
+
def init_label_set(labels)
|
75
|
+
@store.set(labels: label_set_for(labels), val: 0)
|
56
76
|
end
|
57
77
|
|
58
78
|
# Returns all label sets with their values
|
@@ -1,11 +1,15 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
|
3
|
+
require 'base64'
|
3
4
|
require 'thread'
|
4
5
|
require 'net/http'
|
5
6
|
require 'uri'
|
7
|
+
require 'erb'
|
8
|
+
require 'set'
|
6
9
|
|
7
10
|
require 'prometheus/client'
|
8
11
|
require 'prometheus/client/formats/text'
|
12
|
+
require 'prometheus/client/label_set_validator'
|
9
13
|
|
10
14
|
module Prometheus
|
11
15
|
# Client is a ruby implementation for a Prometheus compatible client.
|
@@ -13,23 +17,41 @@ module Prometheus
|
|
13
17
|
# Push implements a simple way to transmit a given registry to a given
|
14
18
|
# Pushgateway.
|
15
19
|
class Push
|
20
|
+
class HttpError < StandardError; end
|
21
|
+
class HttpRedirectError < HttpError; end
|
22
|
+
class HttpClientError < HttpError; end
|
23
|
+
class HttpServerError < HttpError; end
|
24
|
+
|
16
25
|
DEFAULT_GATEWAY = 'http://localhost:9091'.freeze
|
17
26
|
PATH = '/metrics/job/%s'.freeze
|
18
|
-
INSTANCE_PATH = '/metrics/job/%s/instance/%s'.freeze
|
19
27
|
SUPPORTED_SCHEMES = %w(http https).freeze
|
20
28
|
|
21
|
-
attr_reader :job, :
|
29
|
+
attr_reader :job, :gateway, :path
|
30
|
+
|
31
|
+
def initialize(job:, gateway: DEFAULT_GATEWAY, grouping_key: {}, **kwargs)
|
32
|
+
raise ArgumentError, "job cannot be nil" if job.nil?
|
33
|
+
raise ArgumentError, "job cannot be empty" if job.empty?
|
34
|
+
@validator = LabelSetValidator.new(expected_labels: grouping_key.keys)
|
35
|
+
@validator.validate_symbols!(grouping_key)
|
22
36
|
|
23
|
-
def initialize(job, instance = nil, gateway = nil)
|
24
37
|
@mutex = Mutex.new
|
25
38
|
@job = job
|
26
|
-
@instance = instance
|
27
39
|
@gateway = gateway || DEFAULT_GATEWAY
|
28
|
-
@
|
40
|
+
@grouping_key = grouping_key
|
41
|
+
@path = build_path(job, grouping_key)
|
42
|
+
|
29
43
|
@uri = parse("#{@gateway}#{@path}")
|
44
|
+
validate_no_basic_auth!(@uri)
|
30
45
|
|
31
46
|
@http = Net::HTTP.new(@uri.host, @uri.port)
|
32
47
|
@http.use_ssl = (@uri.scheme == 'https')
|
48
|
+
@http.open_timeout = kwargs[:open_timeout] if kwargs[:open_timeout]
|
49
|
+
@http.read_timeout = kwargs[:read_timeout] if kwargs[:read_timeout]
|
50
|
+
end
|
51
|
+
|
52
|
+
def basic_auth(user, password)
|
53
|
+
@user = user
|
54
|
+
@password = password
|
33
55
|
end
|
34
56
|
|
35
57
|
def add(registry)
|
@@ -64,26 +86,118 @@ module Prometheus
|
|
64
86
|
raise ArgumentError, "#{url} is not a valid URL: #{e}"
|
65
87
|
end
|
66
88
|
|
67
|
-
def build_path(job,
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
89
|
+
def build_path(job, grouping_key)
|
90
|
+
path = format(PATH, ERB::Util::url_encode(job))
|
91
|
+
|
92
|
+
grouping_key.each do |label, value|
|
93
|
+
if value.include?('/')
|
94
|
+
encoded_value = Base64.urlsafe_encode64(value)
|
95
|
+
path += "/#{label}@base64/#{encoded_value}"
|
96
|
+
# While it's valid for the urlsafe_encode64 function to return an
|
97
|
+
# empty string when the input string is empty, it doesn't work for
|
98
|
+
# our specific use case as we're putting the result into a URL path
|
99
|
+
# segment. A double slash (`//`) can be normalised away by HTTP
|
100
|
+
# libraries, proxies, and web servers.
|
101
|
+
#
|
102
|
+
# For empty strings, we use a single padding character (`=`) as the
|
103
|
+
# value.
|
104
|
+
#
|
105
|
+
# See the pushgateway docs for more details:
|
106
|
+
#
|
107
|
+
# https://github.com/prometheus/pushgateway/blob/6393a901f56d4dda62cd0f6ab1f1f07c495b6354/README.md#url
|
108
|
+
elsif value.empty?
|
109
|
+
path += "/#{label}@base64/="
|
110
|
+
else
|
111
|
+
path += "/#{label}/#{ERB::Util::url_encode(value)}"
|
112
|
+
end
|
72
113
|
end
|
114
|
+
|
115
|
+
path
|
73
116
|
end
|
74
117
|
|
75
118
|
def request(req_class, registry = nil)
|
119
|
+
validate_no_label_clashes!(registry) if registry
|
120
|
+
|
76
121
|
req = req_class.new(@uri)
|
77
122
|
req.content_type = Formats::Text::CONTENT_TYPE
|
78
|
-
req.basic_auth(@
|
123
|
+
req.basic_auth(@user, @password) if @user
|
79
124
|
req.body = Formats::Text.marshal(registry) if registry
|
80
125
|
|
81
|
-
@http.request(req)
|
126
|
+
response = @http.request(req)
|
127
|
+
validate_response!(response)
|
128
|
+
|
129
|
+
response
|
82
130
|
end
|
83
131
|
|
84
132
|
def synchronize
|
85
133
|
@mutex.synchronize { yield }
|
86
134
|
end
|
135
|
+
|
136
|
+
def validate_no_basic_auth!(uri)
|
137
|
+
if uri.user || uri.password
|
138
|
+
raise ArgumentError, <<~EOF
|
139
|
+
Setting Basic Auth credentials in the gateway URL is not supported, please call the `basic_auth` method.
|
140
|
+
|
141
|
+
Received username `#{uri.user}` in gateway URL. Instead of passing
|
142
|
+
Basic Auth credentials like this:
|
143
|
+
|
144
|
+
```
|
145
|
+
push = Prometheus::Client::Push.new(job: "my-job", gateway: "http://user:password@localhost:9091")
|
146
|
+
```
|
147
|
+
|
148
|
+
please pass them like this:
|
149
|
+
|
150
|
+
```
|
151
|
+
push = Prometheus::Client::Push.new(job: "my-job", gateway: "http://localhost:9091")
|
152
|
+
push.basic_auth("user", "password")
|
153
|
+
```
|
154
|
+
|
155
|
+
While URLs do support passing Basic Auth credentials using the
|
156
|
+
`http://user:password@example.com/` syntax, the username and
|
157
|
+
password in that syntax have to follow the usual rules for URL
|
158
|
+
encoding of characters per RFC 3986
|
159
|
+
(https://datatracker.ietf.org/doc/html/rfc3986#section-2.1).
|
160
|
+
|
161
|
+
Rather than place the burden of correctly performing that encoding
|
162
|
+
on users of this gem, we decided to have a separate method for
|
163
|
+
supplying Basic Auth credentials, with no requirement to URL encode
|
164
|
+
the characters in them.
|
165
|
+
EOF
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def validate_no_label_clashes!(registry)
|
170
|
+
# There's nothing to check if we don't have a grouping key
|
171
|
+
return if @grouping_key.empty?
|
172
|
+
|
173
|
+
# We could be doing a lot of comparisons, so let's do them against a
|
174
|
+
# set rather than an array
|
175
|
+
grouping_key_labels = @grouping_key.keys.to_set
|
176
|
+
|
177
|
+
registry.metrics.each do |metric|
|
178
|
+
metric.labels.each do |label|
|
179
|
+
if grouping_key_labels.include?(label)
|
180
|
+
raise LabelSetValidator::InvalidLabelSetError,
|
181
|
+
"label :#{label} from grouping key collides with label of the " \
|
182
|
+
"same name from metric :#{metric.name} and would overwrite it"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def validate_response!(response)
|
189
|
+
status = Integer(response.code)
|
190
|
+
if status >= 300
|
191
|
+
message = "status: #{response.code}, message: #{response.message}, body: #{response.body}"
|
192
|
+
if status <= 399
|
193
|
+
raise HttpRedirectError, message
|
194
|
+
elsif status <= 499
|
195
|
+
raise HttpClientError, message
|
196
|
+
else
|
197
|
+
raise HttpServerError, message
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
87
201
|
end
|
88
202
|
end
|
89
203
|
end
|
@@ -22,7 +22,7 @@ module Prometheus
|
|
22
22
|
name = metric.name
|
23
23
|
|
24
24
|
@mutex.synchronize do
|
25
|
-
if
|
25
|
+
if @metrics.key?(name.to_sym)
|
26
26
|
raise AlreadyRegisteredError, "#{name} has already been registered"
|
27
27
|
end
|
28
28
|
@metrics[name.to_sym] = metric
|
@@ -73,15 +73,15 @@ module Prometheus
|
|
73
73
|
end
|
74
74
|
|
75
75
|
def exist?(name)
|
76
|
-
@metrics.key?(name)
|
76
|
+
@mutex.synchronize { @metrics.key?(name) }
|
77
77
|
end
|
78
78
|
|
79
79
|
def get(name)
|
80
|
-
@metrics[name.to_sym]
|
80
|
+
@mutex.synchronize { @metrics[name.to_sym] }
|
81
81
|
end
|
82
82
|
|
83
83
|
def metrics
|
84
|
-
@metrics.values
|
84
|
+
@mutex.synchronize { @metrics.values }
|
85
85
|
end
|
86
86
|
end
|
87
87
|
end
|
@@ -11,7 +11,12 @@ module Prometheus
|
|
11
11
|
:summary
|
12
12
|
end
|
13
13
|
|
14
|
-
# Records a given value.
|
14
|
+
# Records a given value. The recorded value is usually positive
|
15
|
+
# or zero. A negative value is accepted but prevents current
|
16
|
+
# versions of Prometheus from properly detecting counter resets
|
17
|
+
# in the sum of observations. See
|
18
|
+
# https://prometheus.io/docs/practices/histograms/#count-and-sum-of-observations
|
19
|
+
# for details.
|
15
20
|
def observe(value, labels: {})
|
16
21
|
base_label_set = label_set_for(labels)
|
17
22
|
|
@@ -36,15 +41,24 @@ module Prometheus
|
|
36
41
|
|
37
42
|
# Returns all label sets with their values expressed as hashes with their sum/count
|
38
43
|
def values
|
39
|
-
|
44
|
+
values = @store.all_values
|
40
45
|
|
41
|
-
|
46
|
+
values.each_with_object({}) do |(label_set, v), acc|
|
42
47
|
actual_label_set = label_set.reject{|l| l == :quantile }
|
43
48
|
acc[actual_label_set] ||= { "count" => 0.0, "sum" => 0.0 }
|
44
49
|
acc[actual_label_set][label_set[:quantile]] = v
|
45
50
|
end
|
46
51
|
end
|
47
52
|
|
53
|
+
def init_label_set(labels)
|
54
|
+
base_label_set = label_set_for(labels)
|
55
|
+
|
56
|
+
@store.synchronize do
|
57
|
+
@store.set(labels: base_label_set.merge(quantile: "count"), val: 0)
|
58
|
+
@store.set(labels: base_label_set.merge(quantile: "sum"), val: 0)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
48
62
|
private
|
49
63
|
|
50
64
|
def reserved_labels
|
@@ -67,15 +67,17 @@ module Prometheus
|
|
67
67
|
end
|
68
68
|
|
69
69
|
def record(env, code, duration)
|
70
|
+
path = generate_path(env)
|
71
|
+
|
70
72
|
counter_labels = {
|
71
73
|
code: code,
|
72
74
|
method: env['REQUEST_METHOD'].downcase,
|
73
|
-
path: strip_ids_from_path(
|
75
|
+
path: strip_ids_from_path(path),
|
74
76
|
}
|
75
77
|
|
76
78
|
duration_labels = {
|
77
79
|
method: env['REQUEST_METHOD'].downcase,
|
78
|
-
path: strip_ids_from_path(
|
80
|
+
path: strip_ids_from_path(path),
|
79
81
|
}
|
80
82
|
|
81
83
|
@requests.increment(labels: counter_labels)
|
@@ -85,10 +87,58 @@ module Prometheus
|
|
85
87
|
nil
|
86
88
|
end
|
87
89
|
|
90
|
+
# While `PATH_INFO` is framework agnostic, and works for any Rack app, some Ruby web
|
91
|
+
# frameworks pass a more useful piece of information into the request env - the
|
92
|
+
# route that the request matched.
|
93
|
+
#
|
94
|
+
# This means that rather than using our generic `:id` and `:uuid` replacements in
|
95
|
+
# the `path` label for any path segments that look like dynamic IDs, we can put the
|
96
|
+
# actual route that matched in there, with correctly named parameters. For example,
|
97
|
+
# if a Sinatra app defined a route like:
|
98
|
+
#
|
99
|
+
# get "/foo/:bar" do
|
100
|
+
# ...
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# instead of containing `/foo/:id`, the `path` label would contain `/foo/:bar`.
|
104
|
+
#
|
105
|
+
# Sadly, Rails is a notable exception, and (as far as I can tell at the time of
|
106
|
+
# writing) doesn't provide this info in the request env.
|
107
|
+
def generate_path(env)
|
108
|
+
if env['sinatra.route']
|
109
|
+
route = env['sinatra.route'].partition(' ').last
|
110
|
+
elsif env['grape.routing_args']
|
111
|
+
# We are deep in the weeds of an object that Grape passes into the request env,
|
112
|
+
# but don't document any explicit guarantees about. Let's have a fallback in
|
113
|
+
# case they change it down the line.
|
114
|
+
#
|
115
|
+
# This code would be neater with the safe navigation operator (`&.`) here rather
|
116
|
+
# than the much more verbose `respond_to?` calls, but unlike Rails' `try`
|
117
|
+
# method, it still raises an error if the object is non-nil, but doesn't respond
|
118
|
+
# to the method being called on it.
|
119
|
+
route = nil
|
120
|
+
|
121
|
+
route_info = env.dig('grape.routing_args', :route_info)
|
122
|
+
if route_info.respond_to?(:pattern)
|
123
|
+
pattern = route_info.pattern
|
124
|
+
if pattern.respond_to?(:origin)
|
125
|
+
route = pattern.origin
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Fall back to PATH_INFO if Grape change the structure of `grape.routing_args`
|
130
|
+
route ||= env['PATH_INFO']
|
131
|
+
else
|
132
|
+
route = env['PATH_INFO']
|
133
|
+
end
|
134
|
+
|
135
|
+
[env['SCRIPT_NAME'], route].join
|
136
|
+
end
|
137
|
+
|
88
138
|
def strip_ids_from_path(path)
|
89
139
|
path
|
90
|
-
.gsub(%r{/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(
|
91
|
-
.gsub(%r{/\d+(
|
140
|
+
.gsub(%r{/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(?=/|$)}, '/:uuid\\1')
|
141
|
+
.gsub(%r{/\d+(?=/|$)}, '/:id\\1')
|
92
142
|
end
|
93
143
|
end
|
94
144
|
end
|
@@ -21,11 +21,12 @@ module Prometheus
|
|
21
21
|
@app = app
|
22
22
|
@registry = options[:registry] || Client.registry
|
23
23
|
@path = options[:path] || '/metrics'
|
24
|
+
@port = options[:port]
|
24
25
|
@acceptable = build_dictionary(FORMATS, FALLBACK)
|
25
26
|
end
|
26
27
|
|
27
28
|
def call(env)
|
28
|
-
if env['PATH_INFO'] == @path
|
29
|
+
if metrics_port?(env['SERVER_PORT']) && env['PATH_INFO'] == @path
|
29
30
|
format = negotiate(env, @acceptable)
|
30
31
|
format ? respond_with(format) : not_acceptable(FORMATS)
|
31
32
|
else
|
@@ -86,6 +87,10 @@ module Prometheus
|
|
86
87
|
memo[format::MEDIA_TYPE] = format
|
87
88
|
end
|
88
89
|
end
|
90
|
+
|
91
|
+
def metrics_port?(request_port)
|
92
|
+
@port.nil? || @port.to_s == request_port
|
93
|
+
end
|
89
94
|
end
|
90
95
|
end
|
91
96
|
end
|
metadata
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: prometheus-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Kochie
|
8
8
|
- Chris Sinjakli
|
9
9
|
- Daniel Magliola
|
10
|
-
autorequire:
|
10
|
+
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2022-02-05 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: benchmark-ips
|
@@ -40,10 +40,10 @@ dependencies:
|
|
40
40
|
- - ">="
|
41
41
|
- !ruby/object:Gem::Version
|
42
42
|
version: '0'
|
43
|
-
description:
|
43
|
+
description:
|
44
44
|
email:
|
45
45
|
- superq@gmail.com
|
46
|
-
- chris@
|
46
|
+
- chris@sinjakli.co.uk
|
47
47
|
- dmagliola@crystalgears.com
|
48
48
|
executables: []
|
49
49
|
extensions: []
|
@@ -73,7 +73,7 @@ homepage: https://github.com/prometheus/client_ruby
|
|
73
73
|
licenses:
|
74
74
|
- Apache 2.0
|
75
75
|
metadata: {}
|
76
|
-
post_install_message:
|
76
|
+
post_install_message:
|
77
77
|
rdoc_options: []
|
78
78
|
require_paths:
|
79
79
|
- lib
|
@@ -84,13 +84,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
84
84
|
version: '0'
|
85
85
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- - "
|
87
|
+
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version:
|
89
|
+
version: '0'
|
90
90
|
requirements: []
|
91
|
-
|
92
|
-
|
93
|
-
signing_key:
|
91
|
+
rubygems_version: 3.2.32
|
92
|
+
signing_key:
|
94
93
|
specification_version: 4
|
95
94
|
summary: A suite of instrumentation metric primitivesthat can be exposed through a
|
96
95
|
web services interface.
|