promenade 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +18 -0
- data/lib/promenade/client/rack/http_request_duration_collector.rb +92 -0
- data/lib/promenade/client/rack/http_request_queue_time_collector.rb +55 -0
- data/lib/promenade/client/rack/middleware_base.rb +36 -0
- data/lib/promenade/client/rack/queue_time_duration.rb +50 -0
- data/lib/promenade/configuration.rb +4 -1
- data/lib/promenade/railtie.rb +5 -2
- data/lib/promenade/version.rb +1 -1
- metadata +7 -4
- data/lib/promenade/client/rack/collector.rb +0 -113
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 235f95d0b77870a9975ff9870428ed5a995b9174380b981c2df7d8a338519ffb
|
4
|
+
data.tar.gz: 7d6e327e2d602fd2b07342fb45c9be29e36ad266212ad4ed98a72eafee58bc9e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e8596f53e3110b636696cb38f2d3bfa872715de3e331fdf260da62e30c31949a1753702b935c3343545c7be19da56f0e30a65d89568eefd419b94a5ef80fe2f9
|
7
|
+
data.tar.gz: 7e65f494c50b2bc25b05b169794f73c02c64ad1548c3425399b1d2cb4e7077d930e0e7d9c99ea88088c76e1026bb64cfbda246ab1a6b09c057b4f2b717b130d8
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
promenade (0.
|
4
|
+
promenade (0.6.0)
|
5
5
|
actionpack
|
6
6
|
activesupport (> 6.0, < 8.0)
|
7
7
|
prometheus-client-mmap (~> 0.16.0)
|
@@ -140,7 +140,7 @@ GEM
|
|
140
140
|
parallel (1.22.1)
|
141
141
|
parser (3.1.2.0)
|
142
142
|
ast (~> 2.4.1)
|
143
|
-
prometheus-client-mmap (0.16.
|
143
|
+
prometheus-client-mmap (0.16.2)
|
144
144
|
pry (0.14.1)
|
145
145
|
coderay (~> 1.1)
|
146
146
|
method_source (~> 1.0)
|
data/README.md
CHANGED
@@ -181,6 +181,20 @@ Rails.application.config.middleware.insert_after ActionDispatch::ShowExceptions,
|
|
181
181
|
exception_handler: exception_handler
|
182
182
|
```
|
183
183
|
|
184
|
+
#### Customising the histogram buckets
|
185
|
+
|
186
|
+
The default buckets cover a range of latencies from 5 ms to 10s see [Promenade::Configuration::DEFAULT_RACK_LATENCY_BUCKETS](https://github.com/errm/promenade/blob/ea7eb54c04257770a601b7e28b3e13db5d2430bb/lib/promenade/configuration.rb#L5). This is intended to capture the typical range of latencies for a web application. However, this might not be suitable for your Service-Level Agreements (SLAs), and other bucket size intervals may be required (see [histogram bins](https://en.wikipedia.org/wiki/Histogram#Number_of_bins_and_width)).
|
187
|
+
|
188
|
+
If you would like to customise the histogram buckets, you can do so by configuring Promenade in an initializer:
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
# config/initializers/promenade.rb
|
192
|
+
|
193
|
+
Promenade.configure do |config|
|
194
|
+
config.rack_latency_buckets = [0.25, 0.350, 0.5, 1, 1.5, 2.5, 5, 10, 15, 19]
|
195
|
+
end
|
196
|
+
```
|
197
|
+
|
184
198
|
### Configuration
|
185
199
|
|
186
200
|
If you are using rails it should load a railtie and configure promenade.
|
@@ -205,6 +219,10 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/errm/p
|
|
205
219
|
|
206
220
|
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
207
221
|
|
222
|
+
## Acknowledgements
|
223
|
+
|
224
|
+
The original code for the Rack middleware collector class was copied from [Prometheus Client MMap](https://gitlab.com/gitlab-org/prometheus-client-mmap/-/blob/master/lib/prometheus/client/rack/collector.rb).
|
225
|
+
|
208
226
|
## License
|
209
227
|
|
210
228
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require "prometheus/client"
|
2
|
+
require_relative "middleware_base"
|
3
|
+
require_relative "request_labeler"
|
4
|
+
require_relative "exception_handler"
|
5
|
+
require_relative "queue_time_duration"
|
6
|
+
|
7
|
+
module Promenade
|
8
|
+
module Client
|
9
|
+
module Rack
|
10
|
+
class HTTPRequestDurationCollector < MiddlwareBase
|
11
|
+
REQUEST_DURATION_HISTOGRAM_NAME = :http_req_duration_seconds
|
12
|
+
|
13
|
+
REQUESTS_COUNTER_NAME = :http_requests_total
|
14
|
+
|
15
|
+
EXCEPTIONS_COUNTER_NAME = :http_exceptions_total
|
16
|
+
|
17
|
+
private_constant :REQUEST_DURATION_HISTOGRAM_NAME,
|
18
|
+
:REQUESTS_COUNTER_NAME,
|
19
|
+
:EXCEPTIONS_COUNTER_NAME
|
20
|
+
|
21
|
+
def initialize(app,
|
22
|
+
registry: ::Prometheus::Client.registry,
|
23
|
+
label_builder: RequestLabeler,
|
24
|
+
exception_handler: nil)
|
25
|
+
|
26
|
+
@latency_buckets = Promenade.configuration.rack_latency_buckets
|
27
|
+
@_exception_handler = exception_handler
|
28
|
+
|
29
|
+
super(app, registry: registry, label_builder: label_builder)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :latency_buckets, :queue_time_buckets
|
35
|
+
|
36
|
+
def trace(env)
|
37
|
+
start = current_time
|
38
|
+
begin
|
39
|
+
response = yield
|
40
|
+
record_request_duration(labels(env, response), duration_since(start))
|
41
|
+
response
|
42
|
+
rescue StandardError => e
|
43
|
+
exception_handler.call(e, env, duration_since(start))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def record_request_duration(labels, duration)
|
48
|
+
requests_counter.increment(labels)
|
49
|
+
durations_histogram.observe(labels, duration)
|
50
|
+
end
|
51
|
+
|
52
|
+
def duration_since(start_time)
|
53
|
+
current_time - start_time
|
54
|
+
end
|
55
|
+
|
56
|
+
def current_time
|
57
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
58
|
+
end
|
59
|
+
|
60
|
+
def durations_histogram
|
61
|
+
registry.get(REQUEST_DURATION_HISTOGRAM_NAME)
|
62
|
+
end
|
63
|
+
|
64
|
+
def requests_counter
|
65
|
+
registry.get(REQUESTS_COUNTER_NAME)
|
66
|
+
end
|
67
|
+
|
68
|
+
def register_metrics!
|
69
|
+
registry.counter(REQUESTS_COUNTER_NAME,
|
70
|
+
"A counter of the total number of HTTP requests made.")
|
71
|
+
registry.histogram(REQUEST_DURATION_HISTOGRAM_NAME,
|
72
|
+
"A histogram of the response latency.", {}, latency_buckets)
|
73
|
+
registry.counter(EXCEPTIONS_COUNTER_NAME,
|
74
|
+
"A counter of the total number of exceptions raised.")
|
75
|
+
end
|
76
|
+
|
77
|
+
def exception_handler
|
78
|
+
@_exception_handler ||= default_exception_handler
|
79
|
+
end
|
80
|
+
|
81
|
+
def default_exception_handler
|
82
|
+
ExceptionHandler.initialize_singleton(
|
83
|
+
histogram_name: REQUEST_DURATION_HISTOGRAM_NAME,
|
84
|
+
requests_counter_name: REQUESTS_COUNTER_NAME,
|
85
|
+
exceptions_counter_name: EXCEPTIONS_COUNTER_NAME,
|
86
|
+
registry: registry,
|
87
|
+
)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "prometheus/client"
|
2
|
+
require_relative "middleware_base"
|
3
|
+
require_relative "request_labeler"
|
4
|
+
require_relative "queue_time_duration"
|
5
|
+
|
6
|
+
module Promenade
|
7
|
+
module Client
|
8
|
+
module Rack
|
9
|
+
class HTTPRequestQueueTimeCollector < MiddlwareBase
|
10
|
+
REQUEST_QUEUE_TIME_HISTOGRAM_NAME = :http_req_queue_time_seconds
|
11
|
+
|
12
|
+
private_constant :REQUEST_QUEUE_TIME_HISTOGRAM_NAME
|
13
|
+
|
14
|
+
def initialize(app,
|
15
|
+
registry: ::Prometheus::Client.registry,
|
16
|
+
label_builder: RequestLabeler)
|
17
|
+
|
18
|
+
@queue_time_buckets = Promenade.configuration.queue_time_buckets
|
19
|
+
|
20
|
+
super(app, registry: registry, label_builder: label_builder)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :queue_time_buckets
|
26
|
+
|
27
|
+
def trace(env)
|
28
|
+
start_timestamp = Time.now.utc
|
29
|
+
response = yield
|
30
|
+
record_request_queue_time(labels: labels(env, response),
|
31
|
+
env: env,
|
32
|
+
request_received_time: start_timestamp)
|
33
|
+
response
|
34
|
+
end
|
35
|
+
|
36
|
+
def record_request_queue_time(labels:, env:, request_received_time:)
|
37
|
+
request_queue_duration = QueueTimeDuration.new(env: env,
|
38
|
+
request_received_time: request_received_time)
|
39
|
+
return unless request_queue_duration.valid_header_present?
|
40
|
+
|
41
|
+
queue_time_histogram.observe(labels, request_queue_duration.queue_time_seconds)
|
42
|
+
end
|
43
|
+
|
44
|
+
def register_metrics!
|
45
|
+
registry.histogram(REQUEST_QUEUE_TIME_HISTOGRAM_NAME,
|
46
|
+
"A histogram of request queue time", {}, queue_time_buckets)
|
47
|
+
end
|
48
|
+
|
49
|
+
def queue_time_histogram
|
50
|
+
registry.get(REQUEST_QUEUE_TIME_HISTOGRAM_NAME)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Promenade
|
2
|
+
module Client
|
3
|
+
module Rack
|
4
|
+
class MiddlwareBase
|
5
|
+
def initialize(app, registry:, label_builder:)
|
6
|
+
@app = app
|
7
|
+
@registry = registry
|
8
|
+
@label_builder = label_builder
|
9
|
+
|
10
|
+
register_metrics!
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
trace(env) { app.call(env) }
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :app, :label_builder, :registry
|
20
|
+
|
21
|
+
def trace(env)
|
22
|
+
raise NotImplementedError,
|
23
|
+
"Please define #{__method__} in #{self.class}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def labels(env, response)
|
27
|
+
label_builder.call(env).merge!(code: response.first.to_s)
|
28
|
+
end
|
29
|
+
|
30
|
+
def register_metrics!
|
31
|
+
# :noop:
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Promenade
|
2
|
+
module Client
|
3
|
+
module Rack
|
4
|
+
class QueueTimeDuration
|
5
|
+
REQUEST_START_HEADER = "HTTP_X_REQUEST_START".freeze
|
6
|
+
|
7
|
+
QUEUE_START_HEADER = "HTTP_X_QUEUE_START".freeze
|
8
|
+
|
9
|
+
HEADER_VALUE_MATCHER = /^(?:t=)(?<timestamp>\d{10}(?:\.\d+))$/.freeze
|
10
|
+
|
11
|
+
def initialize(env:, request_received_time:)
|
12
|
+
@env = env
|
13
|
+
@request_queued_time_ms = extract_request_queued_time_from_env(env)
|
14
|
+
@valid_header_present = @request_queued_time_ms.is_a?(Float)
|
15
|
+
@request_received_time_ms = request_received_time.utc.to_f
|
16
|
+
|
17
|
+
freeze
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid_header_present?
|
21
|
+
@valid_header_present
|
22
|
+
end
|
23
|
+
|
24
|
+
def queue_time_seconds
|
25
|
+
return unless valid_header_present?
|
26
|
+
|
27
|
+
queue_time.round(3)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :env, :request_queued_time_ms, :request_received_time_ms
|
33
|
+
|
34
|
+
def queue_time
|
35
|
+
request_received_time_ms - request_queued_time_ms
|
36
|
+
end
|
37
|
+
|
38
|
+
def extract_request_queued_time_from_env(env_hash)
|
39
|
+
header_value = env_hash[REQUEST_START_HEADER] || env_hash[QUEUE_START_HEADER]
|
40
|
+
return if header_value.nil?
|
41
|
+
|
42
|
+
header_time_match = header_value.to_s.match(HEADER_VALUE_MATCHER)
|
43
|
+
return unless header_time_match
|
44
|
+
|
45
|
+
header_time_match[:timestamp].to_f
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -1,11 +1,14 @@
|
|
1
1
|
module Promenade
|
2
2
|
class Configuration
|
3
|
-
attr_accessor :rack_latency_buckets
|
3
|
+
attr_accessor :queue_time_buckets, :rack_latency_buckets
|
4
4
|
|
5
5
|
DEFAULT_RACK_LATENCY_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10].freeze
|
6
6
|
|
7
|
+
DEFAULT_QUEUE_TIME_BUCKETS = [0.01, 0.5, 1.0, 10.0, 30.0].freeze
|
8
|
+
|
7
9
|
def initialize
|
8
10
|
@rack_latency_buckets = DEFAULT_RACK_LATENCY_BUCKETS
|
11
|
+
@queue_time_buckets = DEFAULT_QUEUE_TIME_BUCKETS
|
9
12
|
end
|
10
13
|
end
|
11
14
|
end
|
data/lib/promenade/railtie.rb
CHANGED
@@ -1,13 +1,16 @@
|
|
1
1
|
require "promenade/setup"
|
2
2
|
require "promenade/engine"
|
3
|
-
require "promenade/client/rack/
|
3
|
+
require "promenade/client/rack/http_request_duration_collector"
|
4
|
+
require "promenade/client/rack/http_request_queue_time_collector"
|
4
5
|
|
5
6
|
module Promenade
|
6
7
|
class Railtie < ::Rails::Railtie
|
7
8
|
initializer "promenade.configure_rails_initialization" do
|
8
9
|
Promenade.setup
|
9
10
|
Rails.application.config.middleware.insert_after ActionDispatch::ShowExceptions,
|
10
|
-
Promenade::Client::Rack::
|
11
|
+
Promenade::Client::Rack::HTTPRequestDurationCollector
|
12
|
+
Rails.application.config.middleware.insert 0,
|
13
|
+
Promenade::Client::Rack::HTTPRequestQueueTimeCollector
|
11
14
|
end
|
12
15
|
end
|
13
16
|
end
|
data/lib/promenade/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: promenade
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ed Robinson
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-08-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -271,8 +271,11 @@ files:
|
|
271
271
|
- bin/setup
|
272
272
|
- exe/promenade
|
273
273
|
- lib/promenade.rb
|
274
|
-
- lib/promenade/client/rack/collector.rb
|
275
274
|
- lib/promenade/client/rack/exception_handler.rb
|
275
|
+
- lib/promenade/client/rack/http_request_duration_collector.rb
|
276
|
+
- lib/promenade/client/rack/http_request_queue_time_collector.rb
|
277
|
+
- lib/promenade/client/rack/middleware_base.rb
|
278
|
+
- lib/promenade/client/rack/queue_time_duration.rb
|
276
279
|
- lib/promenade/client/rack/request_labeler.rb
|
277
280
|
- lib/promenade/client/rack/singleton_caller.rb
|
278
281
|
- lib/promenade/configuration.rb
|
@@ -312,7 +315,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
312
315
|
- !ruby/object:Gem::Version
|
313
316
|
version: '0'
|
314
317
|
requirements: []
|
315
|
-
rubygems_version: 3.
|
318
|
+
rubygems_version: 3.3.18
|
316
319
|
signing_key:
|
317
320
|
specification_version: 4
|
318
321
|
summary: Promenade makes it simple to instrument Ruby apps for prometheus scraping
|
@@ -1,113 +0,0 @@
|
|
1
|
-
require "prometheus/client"
|
2
|
-
require_relative "request_labeler"
|
3
|
-
require_relative "exception_handler"
|
4
|
-
|
5
|
-
module Promenade
|
6
|
-
module Client
|
7
|
-
module Rack
|
8
|
-
# Original code taken from Prometheus Client MMap
|
9
|
-
# https://gitlab.com/gitlab-org/prometheus-client-mmap/-/blob/master/lib/prometheus/client/rack/collector.rb
|
10
|
-
#
|
11
|
-
# Collector is a Rack middleware that provides a sample implementation of
|
12
|
-
# a HTTP tracer. The default label builder can be modified to export a
|
13
|
-
# different set of labels per recorded metric.
|
14
|
-
class Collector
|
15
|
-
REQUEST_METHOD = "REQUEST_METHOD".freeze
|
16
|
-
|
17
|
-
HTTP_HOST = "HTTP_HOST".freeze
|
18
|
-
|
19
|
-
PATH_INFO = "PATH_INFO".freeze
|
20
|
-
|
21
|
-
HISTOGRAM_NAME = :http_req_duration_seconds
|
22
|
-
|
23
|
-
REQUESTS_COUNTER_NAME = :http_requests_total
|
24
|
-
|
25
|
-
EXCEPTIONS_COUNTER_NAME = :http_exceptions_total
|
26
|
-
|
27
|
-
private_constant *%i(
|
28
|
-
REQUEST_METHOD
|
29
|
-
HTTP_HOST
|
30
|
-
PATH_INFO
|
31
|
-
HISTOGRAM_NAME
|
32
|
-
REQUESTS_COUNTER_NAME
|
33
|
-
EXCEPTIONS_COUNTER_NAME
|
34
|
-
)
|
35
|
-
|
36
|
-
def initialize(app,
|
37
|
-
registry: ::Prometheus::Client.registry,
|
38
|
-
label_builder: RequestLabeler,
|
39
|
-
exception_handler: nil)
|
40
|
-
@app = app
|
41
|
-
@registry = registry
|
42
|
-
@label_builder = label_builder
|
43
|
-
@latency_buckets = Promenade.configuration.rack_latency_buckets
|
44
|
-
@exception_handler = exception_handler || default_exception_handler
|
45
|
-
register_metrics!
|
46
|
-
end
|
47
|
-
|
48
|
-
def call(env)
|
49
|
-
trace(env) { app.call(env) }
|
50
|
-
end
|
51
|
-
|
52
|
-
private
|
53
|
-
|
54
|
-
attr_reader :app,
|
55
|
-
:registry,
|
56
|
-
:label_builder,
|
57
|
-
:latency_buckets,
|
58
|
-
:exception_handler
|
59
|
-
|
60
|
-
def trace(env)
|
61
|
-
start = current_time
|
62
|
-
begin
|
63
|
-
response = yield
|
64
|
-
record(labels(env, response), duration_since(start))
|
65
|
-
response
|
66
|
-
rescue StandardError => e
|
67
|
-
exception_handler.call(e, env, duration_since(start))
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def labels(env, response)
|
72
|
-
label_builder.call(env).merge!(code: response.first.to_s)
|
73
|
-
end
|
74
|
-
|
75
|
-
def record(labels, duration)
|
76
|
-
requests_counter.increment(labels)
|
77
|
-
durations_histogram.observe(labels, duration)
|
78
|
-
end
|
79
|
-
|
80
|
-
def duration_since(start_time)
|
81
|
-
current_time - start_time
|
82
|
-
end
|
83
|
-
|
84
|
-
def current_time
|
85
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
86
|
-
end
|
87
|
-
|
88
|
-
def durations_histogram
|
89
|
-
registry.get(HISTOGRAM_NAME)
|
90
|
-
end
|
91
|
-
|
92
|
-
def requests_counter
|
93
|
-
registry.get(REQUESTS_COUNTER_NAME)
|
94
|
-
end
|
95
|
-
|
96
|
-
def register_metrics!
|
97
|
-
registry.counter(REQUESTS_COUNTER_NAME, "A counter of the total number of HTTP requests made.")
|
98
|
-
registry.histogram(HISTOGRAM_NAME, "A histogram of the response latency.", {}, latency_buckets)
|
99
|
-
registry.counter(EXCEPTIONS_COUNTER_NAME, "A counter of the total number of exceptions raised.")
|
100
|
-
end
|
101
|
-
|
102
|
-
def default_exception_handler
|
103
|
-
ExceptionHandler.initialize_singleton(
|
104
|
-
histogram_name: HISTOGRAM_NAME,
|
105
|
-
requests_counter_name: REQUESTS_COUNTER_NAME,
|
106
|
-
exceptions_counter_name: EXCEPTIONS_COUNTER_NAME,
|
107
|
-
registry: registry,
|
108
|
-
)
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|