promenade 0.5.0 → 0.7.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: c8f195d39a6967000ebf64da2a2143a85c934ad0a37b7303eb91511dd1f458bf
4
+ data.tar.gz: '030960d747dd81a33a2feea7b6f3a3460fdc3952160b4c000900645df163d402'
5
5
  SHA512:
6
- metadata.gz: 9fc56c5ea2851e9dfaeae6e56ef63795cab6e2acbd11eed8ecc0e70b9f8198186f92ca74d37afaa92bba579db826b06820fad7b78ee2dcd756aa019d8dd7967f
7
- data.tar.gz: dd5269ee49251537374407c60cf9f31a00d3fcb6ffcfa7c0c0bfa9e8b76d23ca29c868d25ff5732a815e9adef193d73d4da626dd3f223f90d61ae31b640a9340
6
+ metadata.gz: 3cb2cd2eaf07029afd1ac8ad574be62d5b5f848734ea67f1f94a919fe9a6f548c2bcb71d761b92a30077575114a7b505eebe21f20f380af47471401c62b898f6
7
+ data.tar.gz: 0e94c108cd6e98d0f1280e07412e2cb9a41ef7f5a0d7f33735d785c1253424246e46c5fc49ddc0757658301f697272baa1ee882f109327442d13ffa87e6f6429
@@ -9,7 +9,7 @@ jobs:
9
9
  strategy:
10
10
  fail-fast: false
11
11
  matrix:
12
- ruby: ['2.7', '3.0', "3.1"]
12
+ ruby: ['2.7', '3.0', "3.1", "3.2"]
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
15
  - uses: actions/checkout@v2
data/.rubocop.yml CHANGED
@@ -16,3 +16,7 @@ Rails/RakeEnvironment:
16
16
  Enabled: false
17
17
  Rails/EnvironmentVariableAccess:
18
18
  Enabled: false
19
+ RSpec/DescribedClass:
20
+ Enabled: false
21
+ Gemspec/DevelopmentDependencies:
22
+ Enabled: false
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.7.0)
5
5
  actionpack
6
6
  activesupport (> 6.0, < 8.0)
7
7
  prometheus-client-mmap (~> 0.16.0)
@@ -109,6 +109,7 @@ GEM
109
109
  highline (2.0.3)
110
110
  i18n (1.10.0)
111
111
  concurrent-ruby (~> 1.0)
112
+ json (2.6.3)
112
113
  loofah (2.18.0)
113
114
  crass (~> 1.0.2)
114
115
  nokogiri (>= 1.5.9)
@@ -138,9 +139,9 @@ GEM
138
139
  mini_portile2 (~> 2.8.0)
139
140
  racc (~> 1.4)
140
141
  parallel (1.22.1)
141
- parser (3.1.2.0)
142
+ parser (3.2.1.0)
142
143
  ast (~> 2.4.1)
143
- prometheus-client-mmap (0.16.0)
144
+ prometheus-client-mmap (0.16.2)
144
145
  pry (0.14.1)
145
146
  coderay (~> 1.1)
146
147
  method_source (~> 1.0)
@@ -176,7 +177,7 @@ GEM
176
177
  zeitwerk (~> 2.5)
177
178
  rainbow (3.1.1)
178
179
  rake (13.0.6)
179
- regexp_parser (2.5.0)
180
+ regexp_parser (2.7.0)
180
181
  rexml (3.2.5)
181
182
  rspec (3.11.0)
182
183
  rspec-core (~> 3.11.0)
@@ -199,17 +200,20 @@ GEM
199
200
  rspec-mocks (~> 3.10)
200
201
  rspec-support (~> 3.10)
201
202
  rspec-support (3.11.0)
202
- rubocop (1.30.1)
203
+ rubocop (1.46.0)
204
+ json (~> 2.3)
203
205
  parallel (~> 1.10)
204
- parser (>= 3.1.0.0)
206
+ parser (>= 3.2.0.0)
205
207
  rainbow (>= 2.2.2, < 4.0)
206
208
  regexp_parser (>= 1.8, < 3.0)
207
209
  rexml (>= 3.2.5, < 4.0)
208
- rubocop-ast (>= 1.18.0, < 2.0)
210
+ rubocop-ast (>= 1.26.0, < 2.0)
209
211
  ruby-progressbar (~> 1.7)
210
- unicode-display_width (>= 1.4.0, < 3.0)
211
- rubocop-ast (1.18.0)
212
- parser (>= 3.1.1.0)
212
+ unicode-display_width (>= 2.4.0, < 3.0)
213
+ rubocop-ast (1.27.0)
214
+ parser (>= 3.2.1.0)
215
+ rubocop-capybara (2.17.1)
216
+ rubocop (~> 1.41)
213
217
  rubocop-performance (1.14.2)
214
218
  rubocop (>= 1.7.0, < 2.0)
215
219
  rubocop-ast (>= 0.4.0)
@@ -217,6 +221,9 @@ GEM
217
221
  activesupport (>= 4.2.0)
218
222
  rack (>= 1.1)
219
223
  rubocop (>= 1.7.0, < 2.0)
224
+ rubocop-rspec (2.18.1)
225
+ rubocop (~> 1.33)
226
+ rubocop-capybara (~> 2.17)
220
227
  ruby-progressbar (1.11.0)
221
228
  simplecov (0.21.2)
222
229
  docile (~> 1.1)
@@ -236,7 +243,7 @@ GEM
236
243
  sync
237
244
  tzinfo (2.0.4)
238
245
  concurrent-ruby (~> 1.0)
239
- unicode-display_width (2.1.0)
246
+ unicode-display_width (2.4.2)
240
247
  webrick (1.7.0)
241
248
  websocket-driver (0.7.5)
242
249
  websocket-extensions (>= 0.1.0)
@@ -262,6 +269,7 @@ DEPENDENCIES
262
269
  rubocop
263
270
  rubocop-performance
264
271
  rubocop-rails
272
+ rubocop-rspec
265
273
  simplecov
266
274
  webrick
267
275
 
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).
data/exe/promenade CHANGED
@@ -8,7 +8,7 @@ Promenade.setup
8
8
 
9
9
  app = Rack::Builder.app do
10
10
  use Rack::Deflater
11
- use ::Prometheus::Client::Rack::Exporter
11
+ use Prometheus::Client::Rack::Exporter
12
12
  map "/health" do
13
13
  run lambda { |_env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
14
14
  end
@@ -1,6 +1,6 @@
1
1
  require "action_dispatch/middleware/exception_wrapper"
2
2
  require_relative "singleton_caller"
3
- require_relative "request_labeler"
3
+ require_relative "request_controller_action_labeler"
4
4
 
5
5
  module Promenade
6
6
  module Client
@@ -8,21 +8,19 @@ module Promenade
8
8
  class ExceptionHandler
9
9
  extend SingletonCaller
10
10
 
11
- attr_reader :histogram_name, :requests_counter_name, :exceptions_counter_name, :registry
11
+ attr_reader :histogram_name, :exceptions_counter_name, :registry
12
12
 
13
- def initialize(histogram_name:, requests_counter_name:, exceptions_counter_name:, registry:)
13
+ def initialize(histogram_name:, exceptions_counter_name:, registry:)
14
14
  @histogram_name = histogram_name
15
- @requests_counter_name = requests_counter_name
16
15
  @exceptions_counter_name = exceptions_counter_name
17
16
  @registry = registry
18
17
  end
19
18
 
20
19
  def call(exception, env_hash, duration)
21
- labels = RequestLabeler.call(env_hash)
20
+ labels = RequestControllerActionLabeler.call(env_hash)
22
21
  labels.merge!(code: status_code_for_exception(exception))
23
22
 
24
23
  histogram.observe(labels, duration.to_f)
25
- requests_counter.increment(labels)
26
24
  exceptions_counter.increment(exception: exception.class.name)
27
25
 
28
26
  raise exception
@@ -34,10 +32,6 @@ module Promenade
34
32
  registry.get(histogram_name)
35
33
  end
36
34
 
37
- def requests_counter
38
- registry.get(requests_counter_name)
39
- end
40
-
41
35
  def exceptions_counter
42
36
  registry.get(exceptions_counter_name)
43
37
  end
@@ -0,0 +1,81 @@
1
+ require "prometheus/client"
2
+ require_relative "middleware_base"
3
+ require_relative "request_controller_action_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
+ EXCEPTIONS_COUNTER_NAME = :http_exceptions_total
14
+
15
+ private_constant :REQUEST_DURATION_HISTOGRAM_NAME,
16
+ :EXCEPTIONS_COUNTER_NAME
17
+
18
+ def initialize(app,
19
+ registry: ::Prometheus::Client.registry,
20
+ label_builder: RequestControllerActionLabeler,
21
+ exception_handler: nil)
22
+
23
+ @latency_buckets = Promenade.configuration.rack_latency_buckets
24
+ @_exception_handler = exception_handler
25
+
26
+ super(app, registry: registry, label_builder: label_builder)
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :latency_buckets, :queue_time_buckets
32
+
33
+ def trace(env)
34
+ start = current_time
35
+ begin
36
+ response = yield
37
+ record_request_duration(labels(env, response), duration_since(start))
38
+ response
39
+ rescue StandardError => e
40
+ exception_handler.call(e, env, duration_since(start))
41
+ end
42
+ end
43
+
44
+ def record_request_duration(labels, duration)
45
+ durations_histogram.observe(labels, duration)
46
+ end
47
+
48
+ def duration_since(start_time)
49
+ current_time - start_time
50
+ end
51
+
52
+ def current_time
53
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
54
+ end
55
+
56
+ def durations_histogram
57
+ registry.get(REQUEST_DURATION_HISTOGRAM_NAME)
58
+ end
59
+
60
+ def register_metrics!
61
+ registry.histogram(REQUEST_DURATION_HISTOGRAM_NAME,
62
+ "A histogram of the response latency.", {}, latency_buckets)
63
+ registry.counter(EXCEPTIONS_COUNTER_NAME,
64
+ "A counter of the total number of exceptions raised.")
65
+ end
66
+
67
+ def exception_handler
68
+ @_exception_handler ||= default_exception_handler
69
+ end
70
+
71
+ def default_exception_handler
72
+ ExceptionHandler.initialize_singleton(
73
+ histogram_name: REQUEST_DURATION_HISTOGRAM_NAME,
74
+ exceptions_counter_name: EXCEPTIONS_COUNTER_NAME,
75
+ registry: registry,
76
+ )
77
+ end
78
+ end
79
+ end
80
+ end
81
+ 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
@@ -0,0 +1,42 @@
1
+ require_relative "request_labeler"
2
+ module Promenade
3
+ module Client
4
+ module Rack
5
+ class RequestControllerActionLabeler < RequestLabeler
6
+ PARAMS_KEY = "action_dispatch.request.parameters".freeze
7
+
8
+ PATH_PARAMS_KEY = "action_dispatch.request.path_parameters".freeze
9
+
10
+ CONTROLLER = "controller".freeze
11
+
12
+ ACTION = "action".freeze
13
+
14
+ UNKNOWN = "unknown".freeze
15
+
16
+ SEPARATOR = "#".freeze
17
+
18
+ private_constant :PARAMS_KEY, :CONTROLLER, :ACTION, :UNKNOWN, :SEPARATOR
19
+
20
+ def call(env)
21
+ super.merge({
22
+ controller_action: controller_action_from_env(env),
23
+ })
24
+ end
25
+
26
+ private
27
+
28
+ def controller_action_from_env(env)
29
+ controller = env.dig(PARAMS_KEY, CONTROLLER) ||
30
+ env.dig(PATH_PARAMS_KEY, CONTROLLER.to_sym) ||
31
+ UNKNOWN
32
+
33
+ action = env.dig(PARAMS_KEY, ACTION) ||
34
+ env.dig(PATH_PARAMS_KEY, ACTION.to_sym) ||
35
+ UNKNOWN
36
+
37
+ [controller, action].join(SEPARATOR)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -9,41 +9,14 @@ module Promenade
9
9
 
10
10
  HTTP_HOST = "HTTP_HOST".freeze
11
11
 
12
- PARAMS_KEY = "action_dispatch.request.parameters".freeze
13
-
14
- PATH_PARAMS_KEY = "action_dispatch.request.path_parameters".freeze
15
-
16
- CONTROLLER = "controller".freeze
17
-
18
- ACTION = "action".freeze
19
-
20
- UNKNOWN = "unknown".freeze
21
-
22
- SEPARATOR = "#".freeze
23
-
24
- private_constant :REQUEST_METHOD, :HTTP_HOST, :PARAMS_KEY, :CONTROLLER, :ACTION, :UNKNOWN, :SEPARATOR
12
+ private_constant :REQUEST_METHOD, :HTTP_HOST
25
13
 
26
14
  def call(env)
27
15
  {
28
16
  method: env[REQUEST_METHOD].to_s.downcase,
29
17
  host: env[HTTP_HOST].to_s,
30
- controller_action: controller_action_from_env(env),
31
18
  }
32
19
  end
33
-
34
- private
35
-
36
- def controller_action_from_env(env)
37
- controller = env.dig(PARAMS_KEY, CONTROLLER) ||
38
- env.dig(PATH_PARAMS_KEY, CONTROLLER.to_sym) ||
39
- UNKNOWN
40
-
41
- action = env.dig(PARAMS_KEY, ACTION) ||
42
- env.dig(PATH_PARAMS_KEY, ACTION.to_sym) ||
43
- UNKNOWN
44
-
45
- [controller, action].join(SEPARATOR)
46
- end
47
20
  end
48
21
  end
49
22
  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.7.0".freeze
3
3
  end
data/lib/promenade.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require "promenade/version"
2
2
  require "promenade/setup"
3
3
  require "promenade/configuration"
4
- require "promenade/railtie" if defined? ::Rails::Railtie
4
+ require "promenade/railtie" if defined? Rails::Railtie
5
5
  require "promenade/prometheus"
6
6
 
7
7
  module Promenade
data/promenade.gemspec CHANGED
@@ -2,7 +2,7 @@ lib = File.expand_path("lib", __dir__)
2
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
  require "promenade/version"
4
4
 
5
- Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
5
+ Gem::Specification.new do |spec|
6
6
  spec.name = "promenade"
7
7
  spec.version = Promenade::VERSION
8
8
  spec.authors = ["Ed Robinson"]
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
22
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
23
  spec.require_paths = ["lib"]
24
24
 
25
- spec.required_ruby_version = ">= 2.7", "< 3.2"
25
+ spec.required_ruby_version = ">= 2.7"
26
26
 
27
27
  spec.add_dependency "actionpack"
28
28
  spec.add_dependency "activesupport", "> 6.0", "< 8.0"
@@ -39,6 +39,7 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
39
39
  spec.add_development_dependency "rubocop"
40
40
  spec.add_development_dependency "rubocop-performance"
41
41
  spec.add_development_dependency "rubocop-rails"
42
+ spec.add_development_dependency "rubocop-rspec"
42
43
  spec.add_development_dependency "simplecov"
43
44
  spec.metadata["rubygems_mfa_required"] = "true"
44
45
  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.7.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: 2023-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -232,6 +232,20 @@ dependencies:
232
232
  - - ">="
233
233
  - !ruby/object:Gem::Version
234
234
  version: '0'
235
+ - !ruby/object:Gem::Dependency
236
+ name: rubocop-rspec
237
+ requirement: !ruby/object:Gem::Requirement
238
+ requirements:
239
+ - - ">="
240
+ - !ruby/object:Gem::Version
241
+ version: '0'
242
+ type: :development
243
+ prerelease: false
244
+ version_requirements: !ruby/object:Gem::Requirement
245
+ requirements:
246
+ - - ">="
247
+ - !ruby/object:Gem::Version
248
+ version: '0'
235
249
  - !ruby/object:Gem::Dependency
236
250
  name: simplecov
237
251
  requirement: !ruby/object:Gem::Requirement
@@ -271,8 +285,12 @@ files:
271
285
  - bin/setup
272
286
  - exe/promenade
273
287
  - lib/promenade.rb
274
- - lib/promenade/client/rack/collector.rb
275
288
  - lib/promenade/client/rack/exception_handler.rb
289
+ - lib/promenade/client/rack/http_request_duration_collector.rb
290
+ - lib/promenade/client/rack/http_request_queue_time_collector.rb
291
+ - lib/promenade/client/rack/middleware_base.rb
292
+ - lib/promenade/client/rack/queue_time_duration.rb
293
+ - lib/promenade/client/rack/request_controller_action_labeler.rb
276
294
  - lib/promenade/client/rack/request_labeler.rb
277
295
  - lib/promenade/client/rack/singleton_caller.rb
278
296
  - lib/promenade/configuration.rb
@@ -303,16 +321,13 @@ required_ruby_version: !ruby/object:Gem::Requirement
303
321
  - - ">="
304
322
  - !ruby/object:Gem::Version
305
323
  version: '2.7'
306
- - - "<"
307
- - !ruby/object:Gem::Version
308
- version: '3.2'
309
324
  required_rubygems_version: !ruby/object:Gem::Requirement
310
325
  requirements:
311
326
  - - ">="
312
327
  - !ruby/object:Gem::Version
313
328
  version: '0'
314
329
  requirements: []
315
- rubygems_version: 3.2.32
330
+ rubygems_version: 3.3.7
316
331
  signing_key:
317
332
  specification_version: 4
318
333
  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