promenade 0.5.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21a119e5d3d87a758957487577e0167dfff5f3d9bf04938b9c2b257f91d46c7c
4
- data.tar.gz: 37698954cc28bdfca381663448821c190bfb5e5d4f67373780668c45e90669e5
3
+ metadata.gz: 235f95d0b77870a9975ff9870428ed5a995b9174380b981c2df7d8a338519ffb
4
+ data.tar.gz: 7d6e327e2d602fd2b07342fb45c9be29e36ad266212ad4ed98a72eafee58bc9e
5
5
  SHA512:
6
- metadata.gz: 9fc56c5ea2851e9dfaeae6e56ef63795cab6e2acbd11eed8ecc0e70b9f8198186f92ca74d37afaa92bba579db826b06820fad7b78ee2dcd756aa019d8dd7967f
7
- data.tar.gz: dd5269ee49251537374407c60cf9f31a00d3fcb6ffcfa7c0c0bfa9e8b76d23ca29c868d25ff5732a815e9adef193d73d4da626dd3f223f90d61ae31b640a9340
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.5.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.0)
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
@@ -1,13 +1,16 @@
1
1
  require "promenade/setup"
2
2
  require "promenade/engine"
3
- require "promenade/client/rack/collector"
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::Collector
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
@@ -1,3 +1,3 @@
1
1
  module Promenade
2
- VERSION = "0.5.0".freeze
2
+ VERSION = "0.6.0".freeze
3
3
  end
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.5.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-06-23 00:00:00.000000000 Z
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.2.32
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