promenade 0.2.2 → 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: 8b4b2246b9bdf2b31727cfb898cf680facfe1bfcd9a6c3393eb7237c4c2ef870
4
- data.tar.gz: 64f386aa492f3f953cb80d8de3067e4d2565de9e0d3d9379888596f2d88cd283
3
+ metadata.gz: 235f95d0b77870a9975ff9870428ed5a995b9174380b981c2df7d8a338519ffb
4
+ data.tar.gz: 7d6e327e2d602fd2b07342fb45c9be29e36ad266212ad4ed98a72eafee58bc9e
5
5
  SHA512:
6
- metadata.gz: 7a7e963d14200c730bfbc8d609ff8571dffae97f136ec5ecf3e567877795d1d8e300c8c9256b905263eb73f422e35b46cf6cae211cbab9edf89675ade9ab1f57
7
- data.tar.gz: d2387375708b46e7f9823ba481419cf1f46b0656ce4f3e42485b420635383aaec7953185bdf714f7a4c7ecb4b38edd0a22bd1ca232705876d087a8df05668971
6
+ metadata.gz: e8596f53e3110b636696cb38f2d3bfa872715de3e331fdf260da62e30c31949a1753702b935c3343545c7be19da56f0e30a65d89568eefd419b94a5ef80fe2f9
7
+ data.tar.gz: 7e65f494c50b2bc25b05b169794f73c02c64ad1548c3425399b1d2cb4e7077d930e0e7d9c99ea88088c76e1026bb64cfbda246ab1a6b09c057b4f2b717b130d8
@@ -2,14 +2,14 @@ name: CI
2
2
  on:
3
3
  push:
4
4
  branches:
5
- -master
5
+ - master
6
6
  pull_request:
7
7
  jobs:
8
8
  test:
9
9
  strategy:
10
10
  fail-fast: false
11
11
  matrix:
12
- ruby: ['2.6', '2.7', '3.0']
12
+ ruby: ['2.7', '3.0', "3.1"]
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
15
  - uses: actions/checkout@v2
data/.gitignore CHANGED
@@ -1,14 +1,16 @@
1
+ .rspec_status
2
+ .byebug_history
3
+ .rubocop-https*yml
1
4
  /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
5
  /coverage/
5
6
  /doc/
7
+ /log/
6
8
  /pkg/
9
+ /.sass-cache/
7
10
  /spec/reports/
11
+ /spec/dummy/log
12
+ /spec/dummy/tmp
8
13
  /tmp/
9
- .rubocop-https*yml
10
- .sass-cache/
11
- vendor
12
-
13
- # rspec failure tracking
14
- .rspec_status
14
+ /vendor/
15
+ /.yardoc
16
+ /_yardoc/
data/.rubocop.yml CHANGED
@@ -2,7 +2,7 @@ inherit_from:
2
2
  - https://raw.githubusercontent.com/cookpad/guides/master/.rubocop.yml
3
3
 
4
4
  AllCops:
5
- TargetRubyVersion: 2.5
5
+ TargetRubyVersion: 2.7
6
6
  NewCops: enable
7
7
  Exclude:
8
8
  - vendor/**/*
data/Gemfile.lock CHANGED
@@ -1,29 +1,92 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- promenade (0.2.2)
5
- activesupport
6
- prometheus-client-mmap (~> 0.12.0)
4
+ promenade (0.6.0)
5
+ actionpack
6
+ activesupport (> 6.0, < 8.0)
7
+ prometheus-client-mmap (~> 0.16.0)
7
8
  rack
8
9
 
9
10
  GEM
10
11
  remote: https://rubygems.org/
11
12
  specs:
12
- activesupport (6.1.3.2)
13
+ actioncable (7.0.3)
14
+ actionpack (= 7.0.3)
15
+ activesupport (= 7.0.3)
16
+ nio4r (~> 2.0)
17
+ websocket-driver (>= 0.6.1)
18
+ actionmailbox (7.0.3)
19
+ actionpack (= 7.0.3)
20
+ activejob (= 7.0.3)
21
+ activerecord (= 7.0.3)
22
+ activestorage (= 7.0.3)
23
+ activesupport (= 7.0.3)
24
+ mail (>= 2.7.1)
25
+ net-imap
26
+ net-pop
27
+ net-smtp
28
+ actionmailer (7.0.3)
29
+ actionpack (= 7.0.3)
30
+ actionview (= 7.0.3)
31
+ activejob (= 7.0.3)
32
+ activesupport (= 7.0.3)
33
+ mail (~> 2.5, >= 2.5.4)
34
+ net-imap
35
+ net-pop
36
+ net-smtp
37
+ rails-dom-testing (~> 2.0)
38
+ actionpack (7.0.3)
39
+ actionview (= 7.0.3)
40
+ activesupport (= 7.0.3)
41
+ rack (~> 2.0, >= 2.2.0)
42
+ rack-test (>= 0.6.3)
43
+ rails-dom-testing (~> 2.0)
44
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
45
+ actiontext (7.0.3)
46
+ actionpack (= 7.0.3)
47
+ activerecord (= 7.0.3)
48
+ activestorage (= 7.0.3)
49
+ activesupport (= 7.0.3)
50
+ globalid (>= 0.6.0)
51
+ nokogiri (>= 1.8.5)
52
+ actionview (7.0.3)
53
+ activesupport (= 7.0.3)
54
+ builder (~> 3.1)
55
+ erubi (~> 1.4)
56
+ rails-dom-testing (~> 2.0)
57
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
58
+ activejob (7.0.3)
59
+ activesupport (= 7.0.3)
60
+ globalid (>= 0.3.6)
61
+ activemodel (7.0.3)
62
+ activesupport (= 7.0.3)
63
+ activerecord (7.0.3)
64
+ activemodel (= 7.0.3)
65
+ activesupport (= 7.0.3)
66
+ activestorage (7.0.3)
67
+ actionpack (= 7.0.3)
68
+ activejob (= 7.0.3)
69
+ activerecord (= 7.0.3)
70
+ activesupport (= 7.0.3)
71
+ marcel (~> 1.0)
72
+ mini_mime (>= 1.1.0)
73
+ activesupport (7.0.3)
13
74
  concurrent-ruby (~> 1.0, >= 1.0.2)
14
75
  i18n (>= 1.6, < 2)
15
76
  minitest (>= 5.1)
16
77
  tzinfo (~> 2.0)
17
- zeitwerk (~> 2.3)
18
78
  ast (2.4.2)
19
- backports (3.21.0)
79
+ backports (3.23.0)
20
80
  binding_of_caller (1.0.0)
21
81
  debug_inspector (>= 0.0.1)
22
- climate_control (1.0.0)
23
- codecov (0.5.2)
82
+ builder (3.2.4)
83
+ byebug (11.1.3)
84
+ climate_control (1.1.1)
85
+ codecov (0.6.0)
24
86
  simplecov (>= 0.15, < 0.22)
25
87
  coderay (1.1.3)
26
- concurrent-ruby (1.1.8)
88
+ concurrent-ruby (1.1.10)
89
+ crass (1.0.6)
27
90
  debug_inspector (1.1.0)
28
91
  deep-cover (1.1.0)
29
92
  deep-cover-core (= 1.1.0)
@@ -37,53 +100,120 @@ GEM
37
100
  pry
38
101
  term-ansicolor
39
102
  terminal-table
40
- diff-lcs (1.4.4)
103
+ diff-lcs (1.5.0)
104
+ digest (3.1.0)
41
105
  docile (1.4.0)
106
+ erubi (1.10.0)
107
+ globalid (1.0.0)
108
+ activesupport (>= 5.0)
42
109
  highline (2.0.3)
43
- i18n (1.8.10)
110
+ i18n (1.10.0)
44
111
  concurrent-ruby (~> 1.0)
112
+ loofah (2.18.0)
113
+ crass (~> 1.0.2)
114
+ nokogiri (>= 1.5.9)
115
+ mail (2.7.1)
116
+ mini_mime (>= 0.1.1)
117
+ marcel (1.0.2)
45
118
  method_source (1.0.0)
46
- minitest (5.14.4)
47
- parallel (1.20.1)
48
- parser (3.0.1.1)
119
+ mini_mime (1.1.2)
120
+ mini_portile2 (2.8.0)
121
+ minitest (5.16.0)
122
+ net-imap (0.2.3)
123
+ digest
124
+ net-protocol
125
+ strscan
126
+ net-pop (0.1.1)
127
+ digest
128
+ net-protocol
129
+ timeout
130
+ net-protocol (0.1.3)
131
+ timeout
132
+ net-smtp (0.3.1)
133
+ digest
134
+ net-protocol
135
+ timeout
136
+ nio4r (2.5.8)
137
+ nokogiri (1.13.6)
138
+ mini_portile2 (~> 2.8.0)
139
+ racc (~> 1.4)
140
+ parallel (1.22.1)
141
+ parser (3.1.2.0)
49
142
  ast (~> 2.4.1)
50
- prometheus-client-mmap (0.12.0)
143
+ prometheus-client-mmap (0.16.2)
51
144
  pry (0.14.1)
52
145
  coderay (~> 1.1)
53
146
  method_source (~> 1.0)
54
- rack (2.2.3)
55
- rainbow (3.0.0)
56
- rake (13.0.3)
57
- regexp_parser (2.1.1)
147
+ racc (1.6.0)
148
+ rack (2.2.3.1)
149
+ rack-test (1.1.0)
150
+ rack (>= 1.0, < 3)
151
+ rails (7.0.3)
152
+ actioncable (= 7.0.3)
153
+ actionmailbox (= 7.0.3)
154
+ actionmailer (= 7.0.3)
155
+ actionpack (= 7.0.3)
156
+ actiontext (= 7.0.3)
157
+ actionview (= 7.0.3)
158
+ activejob (= 7.0.3)
159
+ activemodel (= 7.0.3)
160
+ activerecord (= 7.0.3)
161
+ activestorage (= 7.0.3)
162
+ activesupport (= 7.0.3)
163
+ bundler (>= 1.15.0)
164
+ railties (= 7.0.3)
165
+ rails-dom-testing (2.0.3)
166
+ activesupport (>= 4.2.0)
167
+ nokogiri (>= 1.6)
168
+ rails-html-sanitizer (1.4.3)
169
+ loofah (~> 2.3)
170
+ railties (7.0.3)
171
+ actionpack (= 7.0.3)
172
+ activesupport (= 7.0.3)
173
+ method_source
174
+ rake (>= 12.2)
175
+ thor (~> 1.0)
176
+ zeitwerk (~> 2.5)
177
+ rainbow (3.1.1)
178
+ rake (13.0.6)
179
+ regexp_parser (2.5.0)
58
180
  rexml (3.2.5)
59
- rspec (3.10.0)
60
- rspec-core (~> 3.10.0)
61
- rspec-expectations (~> 3.10.0)
62
- rspec-mocks (~> 3.10.0)
63
- rspec-core (3.10.1)
64
- rspec-support (~> 3.10.0)
65
- rspec-expectations (3.10.1)
181
+ rspec (3.11.0)
182
+ rspec-core (~> 3.11.0)
183
+ rspec-expectations (~> 3.11.0)
184
+ rspec-mocks (~> 3.11.0)
185
+ rspec-core (3.11.0)
186
+ rspec-support (~> 3.11.0)
187
+ rspec-expectations (3.11.0)
66
188
  diff-lcs (>= 1.2.0, < 2.0)
67
- rspec-support (~> 3.10.0)
68
- rspec-mocks (3.10.2)
189
+ rspec-support (~> 3.11.0)
190
+ rspec-mocks (3.11.1)
69
191
  diff-lcs (>= 1.2.0, < 2.0)
70
- rspec-support (~> 3.10.0)
71
- rspec-support (3.10.2)
72
- rubocop (1.15.0)
192
+ rspec-support (~> 3.11.0)
193
+ rspec-rails (5.1.2)
194
+ actionpack (>= 5.2)
195
+ activesupport (>= 5.2)
196
+ railties (>= 5.2)
197
+ rspec-core (~> 3.10)
198
+ rspec-expectations (~> 3.10)
199
+ rspec-mocks (~> 3.10)
200
+ rspec-support (~> 3.10)
201
+ rspec-support (3.11.0)
202
+ rubocop (1.30.1)
73
203
  parallel (~> 1.10)
74
- parser (>= 3.0.0.0)
204
+ parser (>= 3.1.0.0)
75
205
  rainbow (>= 2.2.2, < 4.0)
76
206
  regexp_parser (>= 1.8, < 3.0)
77
- rexml
78
- rubocop-ast (>= 1.5.0, < 2.0)
207
+ rexml (>= 3.2.5, < 4.0)
208
+ rubocop-ast (>= 1.18.0, < 2.0)
79
209
  ruby-progressbar (~> 1.7)
80
210
  unicode-display_width (>= 1.4.0, < 3.0)
81
- rubocop-ast (1.5.0)
82
- parser (>= 3.0.1.1)
83
- rubocop-performance (1.11.3)
211
+ rubocop-ast (1.18.0)
212
+ parser (>= 3.1.1.0)
213
+ rubocop-performance (1.14.2)
84
214
  rubocop (>= 1.7.0, < 2.0)
85
215
  rubocop-ast (>= 0.4.0)
86
- rubocop-rails (2.10.1)
216
+ rubocop-rails (2.15.0)
87
217
  activesupport (>= 4.2.0)
88
218
  rack (>= 1.1)
89
219
  rubocop (>= 1.7.0, < 2.0)
@@ -93,34 +223,42 @@ GEM
93
223
  simplecov-html (~> 0.11)
94
224
  simplecov_json_formatter (~> 0.1)
95
225
  simplecov-html (0.12.3)
96
- simplecov_json_formatter (0.1.3)
226
+ simplecov_json_formatter (0.1.4)
227
+ strscan (3.0.3)
97
228
  sync (0.5.0)
98
229
  term-ansicolor (1.7.1)
99
230
  tins (~> 1.0)
100
- terminal-table (3.0.1)
231
+ terminal-table (3.0.2)
101
232
  unicode-display_width (>= 1.1.1, < 3)
102
- thor (1.1.0)
103
- tins (1.29.1)
233
+ thor (1.2.1)
234
+ timeout (0.3.0)
235
+ tins (1.31.1)
104
236
  sync
105
237
  tzinfo (2.0.4)
106
238
  concurrent-ruby (~> 1.0)
107
- unicode-display_width (2.0.0)
239
+ unicode-display_width (2.1.0)
108
240
  webrick (1.7.0)
241
+ websocket-driver (0.7.5)
242
+ websocket-extensions (>= 0.1.0)
243
+ websocket-extensions (0.1.5)
109
244
  with_progress (1.0.1)
110
245
  ruby-progressbar (~> 1.4)
111
- zeitwerk (2.4.2)
246
+ zeitwerk (2.6.0)
112
247
 
113
248
  PLATFORMS
114
249
  ruby
115
250
 
116
251
  DEPENDENCIES
117
252
  bundler (~> 2.0)
253
+ byebug
118
254
  climate_control
119
255
  codecov
120
256
  deep-cover
121
257
  promenade!
122
- rake (~> 13.0)
123
- rspec (~> 3.0)
258
+ rails (> 3.0, < 8.0)
259
+ rake
260
+ rspec (~> 3.11)
261
+ rspec-rails (~> 5.1)
124
262
  rubocop
125
263
  rubocop-performance
126
264
  rubocop-rails
@@ -128,4 +266,4 @@ DEPENDENCIES
128
266
  webrick
129
267
 
130
268
  BUNDLED WITH
131
- 2.1.4
269
+ 2.3.16
data/README.md CHANGED
@@ -129,6 +129,72 @@ This is ideal if you are worried about accidentally exposing your metrics, are c
129
129
 
130
130
  The exporter runs by default on port `9394` and the metrics are available at the standard path of `/metrics`, the stand-alone exporter is configured to use gzip.
131
131
 
132
+
133
+ ### Rails Middleware
134
+
135
+ Promenade provides custom Rack middleware to track HTTP response times for requests in your Rails application.
136
+
137
+ This was originally inspired by [prometheus-client-mmap](https://gitlab.com/gitlab-org/prometheus-client-mmap/-/blob/master/lib/prometheus/client/rack/collector.rb).
138
+
139
+ **This middleware is automatically added to your Rack stack if your application is a Ruby on Rails app.**
140
+
141
+ We recommend you add the middleware after `ActionDispatch::ShowExceptions` in your stack, so you can accurately record the controller and action where an exception was raised.
142
+
143
+ If you want to change the position, or customise the labels and exception handling behaviour, simply remove the middleware from the stack and re-insert it with your own preferences.
144
+
145
+ ``` ruby
146
+ Rails.application.middleware.delete(Promenade::Client::Rack::Collector)
147
+ Rails.application.middleware.insert_after(Rails::Rack::Logger, Promenade::Client::Rack::Collector)
148
+ ```
149
+
150
+ #### Customising the labels recorded for each request
151
+
152
+ If you would like to collect different labels with each request, you may do so by customising the middleware installation:
153
+
154
+ ``` ruby
155
+ label_builder = Proc.new do |env|
156
+ {
157
+ method: env["REQUEST_METHOD"].to_s.downcase,
158
+ host: env["HTTP_HOST"].to_s,
159
+ controller: env.dig("action_dispatch.request.parameters", "controller") || "unknown",
160
+ action: env.dig("action_dispatch.request.parameters", "action") || "unknown"
161
+ }
162
+ end
163
+ Rails.application.config.middleware.insert_after ActionDispatch::ShowExceptions,
164
+ Promenade::Client::Rack::Collector
165
+ label_builder: label_builder
166
+ ```
167
+
168
+ #### Customising how the middleware handles exceptions
169
+
170
+ The default implementation will capture exceptions, count the execption class name (e.g. `"StandardError"`), and then re-raise the exception.
171
+
172
+ If you would like to customise this behaviour, you may do so by customising the middleware installation:
173
+
174
+ ``` ruby
175
+ exception_handler = Proc.new do |exception, exception_counter, env_hash, request_duration_seconds|
176
+ # This simple example just re-raises the execption
177
+ raise exception
178
+ end
179
+ Rails.application.config.middleware.insert_after ActionDispatch::ShowExceptions,
180
+ Promenade::Client::Rack::Collector
181
+ exception_handler: exception_handler
182
+ ```
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
+
132
198
  ### Configuration
133
199
 
134
200
  If you are using rails it should load a railtie and configure promenade.
@@ -153,6 +219,10 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/errm/p
153
219
 
154
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.
155
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
+
156
226
  ## License
157
227
 
158
228
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/bin/integration_test CHANGED
@@ -4,14 +4,23 @@ require "bundler/setup"
4
4
  require "promenade"
5
5
  require "fileutils"
6
6
  require "net/http"
7
+ require "./spec/support/integration_tests/metrics_line"
8
+ require "./spec/support/integration_tests/label_value"
7
9
 
8
10
  def test_http_body(expected)
11
+ expectation = MetricsLine.new(expected)
9
12
  uri = URI("http://localhost:9394/metrics")
10
13
 
11
14
  Net::HTTP.start(uri.host, uri.port) do |http|
12
15
  request = Net::HTTP::Get.new uri
13
16
  response = http.request request
14
- fail "#{response.body} didn't include #{expected}" unless response.body.split("\n").any?(expected)
17
+ unless response.body.each_line.detect do |string|
18
+ next if string.start_with?("#")
19
+
20
+ MetricsLine.new(string) == expectation
21
+ end
22
+ fail "#{response.body} didn't include #{expected}"
23
+ end
15
24
  end
16
25
  end
17
26
 
data/bin/rails ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path("..", __dir__)
6
+ ENGINE_PATH = File.expand_path("../lib/promenade/engine", __dir__)
7
+ APP_PATH = File.expand_path("../spec/dummy/config/application", __dir__)
8
+
9
+ # Set up gems listed in the Gemfile.
10
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
11
+ require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
12
+
13
+ require "rails/all"
14
+ require "rails/engine/commands"
@@ -0,0 +1,51 @@
1
+ require "action_dispatch/middleware/exception_wrapper"
2
+ require_relative "singleton_caller"
3
+ require_relative "request_labeler"
4
+
5
+ module Promenade
6
+ module Client
7
+ module Rack
8
+ class ExceptionHandler
9
+ extend SingletonCaller
10
+
11
+ attr_reader :histogram_name, :requests_counter_name, :exceptions_counter_name, :registry
12
+
13
+ def initialize(histogram_name:, requests_counter_name:, exceptions_counter_name:, registry:)
14
+ @histogram_name = histogram_name
15
+ @requests_counter_name = requests_counter_name
16
+ @exceptions_counter_name = exceptions_counter_name
17
+ @registry = registry
18
+ end
19
+
20
+ def call(exception, env_hash, duration)
21
+ labels = RequestLabeler.call(env_hash)
22
+ labels.merge!(code: status_code_for_exception(exception))
23
+
24
+ histogram.observe(labels, duration.to_f)
25
+ requests_counter.increment(labels)
26
+ exceptions_counter.increment(exception: exception.class.name)
27
+
28
+ raise exception
29
+ end
30
+
31
+ private
32
+
33
+ def histogram
34
+ registry.get(histogram_name)
35
+ end
36
+
37
+ def requests_counter
38
+ registry.get(requests_counter_name)
39
+ end
40
+
41
+ def exceptions_counter
42
+ registry.get(exceptions_counter_name)
43
+ end
44
+
45
+ def status_code_for_exception(exception)
46
+ ActionDispatch::ExceptionWrapper.new(nil, exception).status_code.to_s
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -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
@@ -0,0 +1,50 @@
1
+ module Promenade
2
+ module Client
3
+ module Rack
4
+ class RequestLabeler
5
+ require_relative "singleton_caller"
6
+ extend SingletonCaller
7
+
8
+ REQUEST_METHOD = "REQUEST_METHOD".freeze
9
+
10
+ HTTP_HOST = "HTTP_HOST".freeze
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
25
+
26
+ def call(env)
27
+ {
28
+ method: env[REQUEST_METHOD].to_s.downcase,
29
+ host: env[HTTP_HOST].to_s,
30
+ controller_action: controller_action_from_env(env),
31
+ }
32
+ 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
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,19 @@
1
+ module Promenade
2
+ module Client
3
+ module Rack
4
+ module SingletonCaller
5
+ def initialize_singleton(...)
6
+ @singleton = new(...)
7
+ end
8
+
9
+ def call(...)
10
+ singleton.call(...)
11
+ end
12
+
13
+ def singleton
14
+ @singleton || initialize_singleton
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ module Promenade
2
+ class Configuration
3
+ attr_accessor :queue_time_buckets, :rack_latency_buckets
4
+
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
+
7
+ DEFAULT_QUEUE_TIME_BUCKETS = [0.01, 0.5, 1.0, 10.0, 30.0].freeze
8
+
9
+ def initialize
10
+ @rack_latency_buckets = DEFAULT_RACK_LATENCY_BUCKETS
11
+ @queue_time_buckets = DEFAULT_QUEUE_TIME_BUCKETS
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ module Promenade
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Promenade
4
+ end
5
+ end
@@ -35,7 +35,7 @@ module Promenade
35
35
  class Options
36
36
  BUCKET_PRESETS = {
37
37
  network: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10].freeze,
38
- memory: (0..10).map { |i| 128 * 2**i },
38
+ memory: (0..10).map { |i| 128 * (2**i) },
39
39
  }.freeze
40
40
 
41
41
  def initialize
@@ -1,9 +1,16 @@
1
1
  require "promenade/setup"
2
+ require "promenade/engine"
3
+ require "promenade/client/rack/http_request_duration_collector"
4
+ require "promenade/client/rack/http_request_queue_time_collector"
2
5
 
3
6
  module Promenade
4
7
  class Railtie < ::Rails::Railtie
5
8
  initializer "promenade.configure_rails_initialization" do
6
9
  Promenade.setup
10
+ Rails.application.config.middleware.insert_after ActionDispatch::ShowExceptions,
11
+ Promenade::Client::Rack::HTTPRequestDurationCollector
12
+ Rails.application.config.middleware.insert 0,
13
+ Promenade::Client::Rack::HTTPRequestQueueTimeCollector
7
14
  end
8
15
  end
9
16
  end
@@ -1,16 +1,25 @@
1
1
  require "pathname"
2
2
 
3
3
  module Promenade
4
- def self.root_dir
5
- rails_root = defined?(Rails) && Rails.root
6
- rails_root || Pathname.new(ENV.fetch("RAILS_ROOT", Dir.pwd))
4
+ module_function
5
+
6
+ def root_dir
7
+ if rails_defined?
8
+ Rails.root
9
+ else
10
+ Pathname.new(ENV.fetch("RAILS_ROOT", Dir.pwd))
11
+ end
12
+ end
13
+
14
+ def rails_defined?
15
+ defined?(Rails)
7
16
  end
8
17
 
9
- def self.multiprocess_files_dir
18
+ def multiprocess_files_dir
10
19
  ENV.fetch("PROMETHEUS_MULTIPROC_DIR", root_dir.join("tmp", "promenade"))
11
20
  end
12
21
 
13
- def self.setup
22
+ def setup
14
23
  unless File.directory? multiprocess_files_dir
15
24
  FileUtils.mkdir_p multiprocess_files_dir
16
25
  end
@@ -1,3 +1,3 @@
1
1
  module Promenade
2
- VERSION = "0.2.2".freeze
2
+ VERSION = "0.6.0".freeze
3
3
  end
data/lib/promenade.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "promenade/version"
2
2
  require "promenade/setup"
3
+ require "promenade/configuration"
3
4
  require "promenade/railtie" if defined? ::Rails::Railtie
4
5
  require "promenade/prometheus"
5
6
 
@@ -14,5 +15,13 @@ module Promenade
14
15
  def metric(name)
15
16
  Promenade::Prometheus.metric(name)
16
17
  end
18
+
19
+ def configuration
20
+ @_configuration ||= Configuration.new
21
+ end
22
+
23
+ def configure
24
+ yield(configuration)
25
+ end
17
26
  end
18
27
  end
data/promenade.gemspec CHANGED
@@ -22,19 +22,23 @@ 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.5", "< 4"
25
+ spec.required_ruby_version = ">= 2.7", "< 3.2"
26
26
 
27
- spec.add_dependency "activesupport"
28
- spec.add_dependency "prometheus-client-mmap", "~> 0.12.0"
27
+ spec.add_dependency "actionpack"
28
+ spec.add_dependency "activesupport", "> 6.0", "< 8.0"
29
+ spec.add_dependency "prometheus-client-mmap", "~> 0.16.0"
29
30
  spec.add_dependency "rack"
30
-
31
31
  spec.add_development_dependency "bundler", "~> 2.0"
32
+ spec.add_development_dependency "byebug"
32
33
  spec.add_development_dependency "climate_control"
33
34
  spec.add_development_dependency "deep-cover"
34
- spec.add_development_dependency "rake", "~> 13.0"
35
- spec.add_development_dependency "rspec", "~> 3.0"
35
+ spec.add_development_dependency "rails", "> 3.0", "< 8.0"
36
+ spec.add_development_dependency "rake"
37
+ spec.add_development_dependency "rspec", "~> 3.11"
38
+ spec.add_development_dependency "rspec-rails", "~> 5.1"
36
39
  spec.add_development_dependency "rubocop"
37
40
  spec.add_development_dependency "rubocop-performance"
38
41
  spec.add_development_dependency "rubocop-rails"
39
42
  spec.add_development_dependency "simplecov"
43
+ spec.metadata["rubygems_mfa_required"] = "true"
40
44
  end
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: promenade
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
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: 2021-05-21 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
- name: activesupport
14
+ name: actionpack
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -24,20 +24,40 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">"
32
+ - !ruby/object:Gem::Version
33
+ version: '6.0'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '8.0'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">"
42
+ - !ruby/object:Gem::Version
43
+ version: '6.0'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '8.0'
27
47
  - !ruby/object:Gem::Dependency
28
48
  name: prometheus-client-mmap
29
49
  requirement: !ruby/object:Gem::Requirement
30
50
  requirements:
31
51
  - - "~>"
32
52
  - !ruby/object:Gem::Version
33
- version: 0.12.0
53
+ version: 0.16.0
34
54
  type: :runtime
35
55
  prerelease: false
36
56
  version_requirements: !ruby/object:Gem::Requirement
37
57
  requirements:
38
58
  - - "~>"
39
59
  - !ruby/object:Gem::Version
40
- version: 0.12.0
60
+ version: 0.16.0
41
61
  - !ruby/object:Gem::Dependency
42
62
  name: rack
43
63
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +86,20 @@ dependencies:
66
86
  - - "~>"
67
87
  - !ruby/object:Gem::Version
68
88
  version: '2.0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: byebug
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
69
103
  - !ruby/object:Gem::Dependency
70
104
  name: climate_control
71
105
  requirement: !ruby/object:Gem::Requirement
@@ -94,34 +128,68 @@ dependencies:
94
128
  - - ">="
95
129
  - !ruby/object:Gem::Version
96
130
  version: '0'
131
+ - !ruby/object:Gem::Dependency
132
+ name: rails
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">"
136
+ - !ruby/object:Gem::Version
137
+ version: '3.0'
138
+ - - "<"
139
+ - !ruby/object:Gem::Version
140
+ version: '8.0'
141
+ type: :development
142
+ prerelease: false
143
+ version_requirements: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">"
146
+ - !ruby/object:Gem::Version
147
+ version: '3.0'
148
+ - - "<"
149
+ - !ruby/object:Gem::Version
150
+ version: '8.0'
97
151
  - !ruby/object:Gem::Dependency
98
152
  name: rake
153
+ requirement: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ type: :development
159
+ prerelease: false
160
+ version_requirements: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ - !ruby/object:Gem::Dependency
166
+ name: rspec
99
167
  requirement: !ruby/object:Gem::Requirement
100
168
  requirements:
101
169
  - - "~>"
102
170
  - !ruby/object:Gem::Version
103
- version: '13.0'
171
+ version: '3.11'
104
172
  type: :development
105
173
  prerelease: false
106
174
  version_requirements: !ruby/object:Gem::Requirement
107
175
  requirements:
108
176
  - - "~>"
109
177
  - !ruby/object:Gem::Version
110
- version: '13.0'
178
+ version: '3.11'
111
179
  - !ruby/object:Gem::Dependency
112
- name: rspec
180
+ name: rspec-rails
113
181
  requirement: !ruby/object:Gem::Requirement
114
182
  requirements:
115
183
  - - "~>"
116
184
  - !ruby/object:Gem::Version
117
- version: '3.0'
185
+ version: '5.1'
118
186
  type: :development
119
187
  prerelease: false
120
188
  version_requirements: !ruby/object:Gem::Requirement
121
189
  requirements:
122
190
  - - "~>"
123
191
  - !ruby/object:Gem::Version
124
- version: '3.0'
192
+ version: '5.1'
125
193
  - !ruby/object:Gem::Dependency
126
194
  name: rubocop
127
195
  requirement: !ruby/object:Gem::Requirement
@@ -199,9 +267,19 @@ files:
199
267
  - Rakefile
200
268
  - bin/console
201
269
  - bin/integration_test
270
+ - bin/rails
202
271
  - bin/setup
203
272
  - exe/promenade
204
273
  - lib/promenade.rb
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
279
+ - lib/promenade/client/rack/request_labeler.rb
280
+ - lib/promenade/client/rack/singleton_caller.rb
281
+ - lib/promenade/configuration.rb
282
+ - lib/promenade/engine.rb
205
283
  - lib/promenade/kafka.rb
206
284
  - lib/promenade/kafka/async_producer_subscriber.rb
207
285
  - lib/promenade/kafka/connection_subscriber.rb
@@ -217,7 +295,8 @@ files:
217
295
  homepage: https://github.com/errm/promenade
218
296
  licenses:
219
297
  - MIT
220
- metadata: {}
298
+ metadata:
299
+ rubygems_mfa_required: 'true'
221
300
  post_install_message:
222
301
  rdoc_options: []
223
302
  require_paths:
@@ -226,17 +305,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
226
305
  requirements:
227
306
  - - ">="
228
307
  - !ruby/object:Gem::Version
229
- version: '2.5'
308
+ version: '2.7'
230
309
  - - "<"
231
310
  - !ruby/object:Gem::Version
232
- version: '4'
311
+ version: '3.2'
233
312
  required_rubygems_version: !ruby/object:Gem::Requirement
234
313
  requirements:
235
314
  - - ">="
236
315
  - !ruby/object:Gem::Version
237
316
  version: '0'
238
317
  requirements: []
239
- rubygems_version: 3.1.2
318
+ rubygems_version: 3.3.18
240
319
  signing_key:
241
320
  specification_version: 4
242
321
  summary: Promenade makes it simple to instrument Ruby apps for prometheus scraping