request_queue_time 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c34dfaeafa5d35cbdaad24f12b6957c4561f1fe93e87485a8bab9cf2914c7650
4
+ data.tar.gz: '018a31f583b994331da3068b220ebd86157b61968ac1a1785330b00ddf9722d0'
5
+ SHA512:
6
+ metadata.gz: aeafaa0ec310ee0489b4d2556fee9aa13d0915a211d1df14f055ecc8447135c992919d25597ddc28809da52f9d0c2dee2702c6ca112b2fdaa10c261714e673dc
7
+ data.tar.gz: 535e007a068451606adae13687e01bab8018f77cb1a30ac955a7618c6ebe7150e363c1810046972f6ff96057558b06632349604dab8f99891e2f1d6b5f44d2a3
data/.env.test ADDED
@@ -0,0 +1,5 @@
1
+ RAILS_ENV=test
2
+ DD_SERVICE=request-queue-time
3
+ DD_ENV=stack-name
4
+
5
+ AWS_REGION=eu-west-1
@@ -0,0 +1,28 @@
1
+ name: Ruby
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ pull_request:
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ name: Ruby ${{ matrix.ruby }}
14
+ strategy:
15
+ matrix:
16
+ ruby:
17
+ - "3.3"
18
+ - "3.2"
19
+
20
+ steps:
21
+ - uses: actions/checkout@v3
22
+ - name: Set up Ruby
23
+ uses: ruby/setup-ruby@v1
24
+ with:
25
+ ruby-version: ${{ matrix.ruby }}
26
+ bundler-cache: true
27
+ - name: Run the default task
28
+ run: bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.6
@@ -0,0 +1,8 @@
1
+ {
2
+ "ruby.lint": {
3
+ "standard": true
4
+ },
5
+ "ruby.format": "standard",
6
+ "editor.formatOnSave": true,
7
+ "ruby.useLanguageServer": true
8
+ }
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in request-queue-time-middleware.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
11
+
12
+ gem "sidekiq"
13
+ gem "rails"
14
+ gem "dotenv"
15
+
16
+ gem "standard", "~> 1.3"
data/Gemfile.lock ADDED
@@ -0,0 +1,280 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ request_queue_time (0.2.0)
5
+ aws-sdk-cloudwatch
6
+ dogstatsd-ruby
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ actioncable (7.1.3.3)
12
+ actionpack (= 7.1.3.3)
13
+ activesupport (= 7.1.3.3)
14
+ nio4r (~> 2.0)
15
+ websocket-driver (>= 0.6.1)
16
+ zeitwerk (~> 2.6)
17
+ actionmailbox (7.1.3.3)
18
+ actionpack (= 7.1.3.3)
19
+ activejob (= 7.1.3.3)
20
+ activerecord (= 7.1.3.3)
21
+ activestorage (= 7.1.3.3)
22
+ activesupport (= 7.1.3.3)
23
+ mail (>= 2.7.1)
24
+ net-imap
25
+ net-pop
26
+ net-smtp
27
+ actionmailer (7.1.3.3)
28
+ actionpack (= 7.1.3.3)
29
+ actionview (= 7.1.3.3)
30
+ activejob (= 7.1.3.3)
31
+ activesupport (= 7.1.3.3)
32
+ mail (~> 2.5, >= 2.5.4)
33
+ net-imap
34
+ net-pop
35
+ net-smtp
36
+ rails-dom-testing (~> 2.2)
37
+ actionpack (7.1.3.3)
38
+ actionview (= 7.1.3.3)
39
+ activesupport (= 7.1.3.3)
40
+ nokogiri (>= 1.8.5)
41
+ racc
42
+ rack (>= 2.2.4)
43
+ rack-session (>= 1.0.1)
44
+ rack-test (>= 0.6.3)
45
+ rails-dom-testing (~> 2.2)
46
+ rails-html-sanitizer (~> 1.6)
47
+ actiontext (7.1.3.3)
48
+ actionpack (= 7.1.3.3)
49
+ activerecord (= 7.1.3.3)
50
+ activestorage (= 7.1.3.3)
51
+ activesupport (= 7.1.3.3)
52
+ globalid (>= 0.6.0)
53
+ nokogiri (>= 1.8.5)
54
+ actionview (7.1.3.3)
55
+ activesupport (= 7.1.3.3)
56
+ builder (~> 3.1)
57
+ erubi (~> 1.11)
58
+ rails-dom-testing (~> 2.2)
59
+ rails-html-sanitizer (~> 1.6)
60
+ activejob (7.1.3.3)
61
+ activesupport (= 7.1.3.3)
62
+ globalid (>= 0.3.6)
63
+ activemodel (7.1.3.3)
64
+ activesupport (= 7.1.3.3)
65
+ activerecord (7.1.3.3)
66
+ activemodel (= 7.1.3.3)
67
+ activesupport (= 7.1.3.3)
68
+ timeout (>= 0.4.0)
69
+ activestorage (7.1.3.3)
70
+ actionpack (= 7.1.3.3)
71
+ activejob (= 7.1.3.3)
72
+ activerecord (= 7.1.3.3)
73
+ activesupport (= 7.1.3.3)
74
+ marcel (~> 1.0)
75
+ activesupport (7.1.3.3)
76
+ base64
77
+ bigdecimal
78
+ concurrent-ruby (~> 1.0, >= 1.0.2)
79
+ connection_pool (>= 2.2.5)
80
+ drb
81
+ i18n (>= 1.6, < 2)
82
+ minitest (>= 5.1)
83
+ mutex_m
84
+ tzinfo (~> 2.0)
85
+ ast (2.4.2)
86
+ aws-eventstream (1.3.0)
87
+ aws-partitions (1.937.0)
88
+ aws-sdk-cloudwatch (1.91.0)
89
+ aws-sdk-core (~> 3, >= 3.193.0)
90
+ aws-sigv4 (~> 1.1)
91
+ aws-sdk-core (3.196.1)
92
+ aws-eventstream (~> 1, >= 1.3.0)
93
+ aws-partitions (~> 1, >= 1.651.0)
94
+ aws-sigv4 (~> 1.8)
95
+ jmespath (~> 1, >= 1.6.1)
96
+ aws-sigv4 (1.8.0)
97
+ aws-eventstream (~> 1, >= 1.0.2)
98
+ base64 (0.2.0)
99
+ bigdecimal (3.1.8)
100
+ builder (3.2.4)
101
+ concurrent-ruby (1.3.1)
102
+ connection_pool (2.4.1)
103
+ crass (1.0.6)
104
+ date (3.3.4)
105
+ diff-lcs (1.5.1)
106
+ dogstatsd-ruby (5.6.1)
107
+ dotenv (3.1.2)
108
+ drb (2.2.1)
109
+ erubi (1.12.0)
110
+ globalid (1.2.1)
111
+ activesupport (>= 6.1)
112
+ i18n (1.14.5)
113
+ concurrent-ruby (~> 1.0)
114
+ io-console (0.7.2)
115
+ irb (1.13.1)
116
+ rdoc (>= 4.0.0)
117
+ reline (>= 0.4.2)
118
+ jmespath (1.6.2)
119
+ json (2.7.2)
120
+ language_server-protocol (3.17.0.3)
121
+ lint_roller (1.1.0)
122
+ loofah (2.22.0)
123
+ crass (~> 1.0.2)
124
+ nokogiri (>= 1.12.0)
125
+ mail (2.8.1)
126
+ mini_mime (>= 0.1.1)
127
+ net-imap
128
+ net-pop
129
+ net-smtp
130
+ marcel (1.0.4)
131
+ mini_mime (1.1.5)
132
+ minitest (5.23.1)
133
+ mutex_m (0.2.0)
134
+ net-imap (0.4.11)
135
+ date
136
+ net-protocol
137
+ net-pop (0.1.2)
138
+ net-protocol
139
+ net-protocol (0.2.2)
140
+ timeout
141
+ net-smtp (0.5.0)
142
+ net-protocol
143
+ nio4r (2.7.3)
144
+ nokogiri (1.16.5-arm64-darwin)
145
+ racc (~> 1.4)
146
+ nokogiri (1.16.5-x86_64-linux)
147
+ racc (~> 1.4)
148
+ parallel (1.24.0)
149
+ parser (3.3.0.5)
150
+ ast (~> 2.4.1)
151
+ racc
152
+ psych (5.1.2)
153
+ stringio
154
+ racc (1.7.3)
155
+ rack (3.0.11)
156
+ rack-session (2.0.0)
157
+ rack (>= 3.0.0)
158
+ rack-test (2.1.0)
159
+ rack (>= 1.3)
160
+ rackup (2.1.0)
161
+ rack (>= 3)
162
+ webrick (~> 1.8)
163
+ rails (7.1.3.3)
164
+ actioncable (= 7.1.3.3)
165
+ actionmailbox (= 7.1.3.3)
166
+ actionmailer (= 7.1.3.3)
167
+ actionpack (= 7.1.3.3)
168
+ actiontext (= 7.1.3.3)
169
+ actionview (= 7.1.3.3)
170
+ activejob (= 7.1.3.3)
171
+ activemodel (= 7.1.3.3)
172
+ activerecord (= 7.1.3.3)
173
+ activestorage (= 7.1.3.3)
174
+ activesupport (= 7.1.3.3)
175
+ bundler (>= 1.15.0)
176
+ railties (= 7.1.3.3)
177
+ rails-dom-testing (2.2.0)
178
+ activesupport (>= 5.0.0)
179
+ minitest
180
+ nokogiri (>= 1.6)
181
+ rails-html-sanitizer (1.6.0)
182
+ loofah (~> 2.21)
183
+ nokogiri (~> 1.14)
184
+ railties (7.1.3.3)
185
+ actionpack (= 7.1.3.3)
186
+ activesupport (= 7.1.3.3)
187
+ irb
188
+ rackup (>= 1.0.0)
189
+ rake (>= 12.2)
190
+ thor (~> 1.0, >= 1.2.2)
191
+ zeitwerk (~> 2.6)
192
+ rainbow (3.1.1)
193
+ rake (13.2.1)
194
+ rdoc (6.7.0)
195
+ psych (>= 4.0.0)
196
+ redis-client (0.22.2)
197
+ connection_pool
198
+ regexp_parser (2.9.0)
199
+ reline (0.5.8)
200
+ io-console (~> 0.5)
201
+ rexml (3.2.8)
202
+ strscan (>= 3.0.9)
203
+ rspec (3.13.0)
204
+ rspec-core (~> 3.13.0)
205
+ rspec-expectations (~> 3.13.0)
206
+ rspec-mocks (~> 3.13.0)
207
+ rspec-core (3.13.0)
208
+ rspec-support (~> 3.13.0)
209
+ rspec-expectations (3.13.0)
210
+ diff-lcs (>= 1.2.0, < 2.0)
211
+ rspec-support (~> 3.13.0)
212
+ rspec-mocks (3.13.0)
213
+ diff-lcs (>= 1.2.0, < 2.0)
214
+ rspec-support (~> 3.13.0)
215
+ rspec-support (3.13.1)
216
+ rubocop (1.62.1)
217
+ json (~> 2.3)
218
+ language_server-protocol (>= 3.17.0)
219
+ parallel (~> 1.10)
220
+ parser (>= 3.3.0.2)
221
+ rainbow (>= 2.2.2, < 4.0)
222
+ regexp_parser (>= 1.8, < 3.0)
223
+ rexml (>= 3.2.5, < 4.0)
224
+ rubocop-ast (>= 1.31.1, < 2.0)
225
+ ruby-progressbar (~> 1.7)
226
+ unicode-display_width (>= 2.4.0, < 3.0)
227
+ rubocop-ast (1.31.2)
228
+ parser (>= 3.3.0.4)
229
+ rubocop-performance (1.20.2)
230
+ rubocop (>= 1.48.1, < 2.0)
231
+ rubocop-ast (>= 1.30.0, < 2.0)
232
+ ruby-progressbar (1.13.0)
233
+ sidekiq (7.2.4)
234
+ concurrent-ruby (< 2)
235
+ connection_pool (>= 2.3.0)
236
+ rack (>= 2.2.4)
237
+ redis-client (>= 0.19.0)
238
+ standard (1.35.1)
239
+ language_server-protocol (~> 3.17.0.2)
240
+ lint_roller (~> 1.0)
241
+ rubocop (~> 1.62.0)
242
+ standard-custom (~> 1.0.0)
243
+ standard-performance (~> 1.3)
244
+ standard-custom (1.0.2)
245
+ lint_roller (~> 1.0)
246
+ rubocop (~> 1.50)
247
+ standard-performance (1.3.1)
248
+ lint_roller (~> 1.1)
249
+ rubocop-performance (~> 1.20.2)
250
+ stringio (3.1.0)
251
+ strscan (3.1.0)
252
+ thor (1.3.1)
253
+ timeout (0.4.1)
254
+ tzinfo (2.0.6)
255
+ concurrent-ruby (~> 1.0)
256
+ unicode-display_width (2.5.0)
257
+ webrick (1.8.1)
258
+ websocket-driver (0.7.6)
259
+ websocket-extensions (>= 0.1.0)
260
+ websocket-extensions (0.1.5)
261
+ zeitwerk (2.6.15)
262
+
263
+ PLATFORMS
264
+ arm64-darwin-22
265
+ arm64-darwin-23
266
+ arm64-darwin-24
267
+ x86_64-linux
268
+
269
+ DEPENDENCIES
270
+ bundler (~> 2.4)
271
+ dotenv
272
+ rails
273
+ rake (~> 13.0)
274
+ request_queue_time!
275
+ rspec (~> 3.0)
276
+ sidekiq
277
+ standard (~> 1.3)
278
+
279
+ BUNDLED WITH
280
+ 2.4.7
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # RequestQueueTime::Middleware
2
+
3
+ This gem gives us an indication of the time that a request/job is spent waiting in line to be processed and with that gauge we scale our ECS cluster tasks up and down. A lot of the logic was borrowed from Judoscales ruby gem, but at the time of writing the code we were unable to utilise Judoscale on ECS, however now that the gem is being extracted this is something that is now a product that they offer. However we are unsure about the financial gains that can be gained from using the service, vs our own solution.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem "request_queue_time", git: "https://github.com/Teamtailor/request_queue_time.git", tag: "v0.1.1"
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ ## Usage
18
+
19
+ The following environment variables are required for the metric that is reported to cloudwatch:
20
+
21
+ ```rb
22
+ ENV["DD_SERVICE"]
23
+ ENV["DD_ENV"]
24
+ ```
25
+
26
+ For example:
27
+ ```rb
28
+ ENV["DD_SERVICE"] = "teamtailor"
29
+ ENV["DD_ENV"] = "eu-cyan"
30
+ ```
31
+
32
+ For the Reporter to run, you need to have the following flag set as well:
33
+
34
+ ```rb
35
+ ENV["ECS_SETUP"]
36
+ ```
37
+
38
+ If you want statsd measurements you need to have a the constant `StatsdDdog` defined.
39
+
40
+ A Railtie will insert the middleware first in your stack, but if there are issues or you need more control of the middleware placements you can use the configuration below:
41
+
42
+ If you want it first in the stack:
43
+ ```rb
44
+ Rails.configuration.middleware.insert_before 0, RequestQueueTime::Middleware
45
+ ```
46
+
47
+ The following is required for the sidekiq portion to work though:
48
+ <!-- And add the following to the application reloader: -->
49
+
50
+ ```rb
51
+ Rails.application.reloader.to_prepare do
52
+ ...
53
+ RequestQueueTime::AutoScalingMetrics::SidekiqReporter.enable if ENV["ECS_SETUP"]
54
+ end
55
+ ```
56
+
57
+ ## Development
58
+
59
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` or `bundle exec rake` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
60
+
61
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "request_queue_time/middleware"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,5 @@
1
+ module RequestQueueTime
2
+ class Middleware
3
+ VERSION = "0.2.0"
4
+ end
5
+ end
@@ -0,0 +1,94 @@
1
+ require "request_queue_time/middleware/version"
2
+ require "services/auto_scaling_metrics"
3
+
4
+ module RequestQueueTime
5
+ class Middleware
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ metrics = Metrics.new(env)
12
+
13
+ AutoScalingMetrics::Reporter.start if ENV["ECS_SETUP"]
14
+
15
+ unless metrics.ignore?
16
+ tags = ["request_method:#{env["REQUEST_METHOD"]}"]
17
+
18
+ ActiveSupport::Notifications.instrument("request_queue_time.timings", extra: {
19
+ tags: tags,
20
+ queue_time: metrics.queue_time,
21
+ network_time: metrics.network_time
22
+ })
23
+
24
+ env["request_queue_time"] = metrics.queue_time
25
+ env["request_network_time"] = metrics.network_time
26
+
27
+ if ENV["ECS_SETUP"]
28
+ AutoScalingMetrics::Reporter.instance.track_request_queue_time(metrics.queue_time)
29
+ end
30
+ end
31
+
32
+ @app.call(env)
33
+ end
34
+
35
+ class Metrics
36
+ MILLISECONDS_CUTOFF = Time.new(2000, 1, 1).to_i * 1000
37
+ MICROSECONDS_CUTOFF = MILLISECONDS_CUTOFF * 1000
38
+ NANOSECONDS_CUTOFF = MICROSECONDS_CUTOFF * 1000
39
+ REQUEST_SIZE_LIMIT_BYTES = 100_000
40
+
41
+ attr_reader :network_time, :request_start, :now, :request_size
42
+
43
+ def initialize(env)
44
+ @network_time = env["puma.request_body_wait"].to_i
45
+ @request_start = env["HTTP_X_REQUEST_START"]
46
+ @now = Time.now
47
+ @request_size = env["rack.input"].respond_to?(:size) ? env["rack.input"].size : 0
48
+ end
49
+
50
+ def ignore?
51
+ queue_time.nil? || request_size > REQUEST_SIZE_LIMIT_BYTES
52
+ end
53
+
54
+ def queue_time
55
+ return @queue_time if defined?(@queue_time)
56
+
57
+ start = started_at
58
+ return if start.nil?
59
+
60
+ queue_time = ((now - start) * 1000).to_i
61
+
62
+ # Remove the time Puma was waiting for the request body.
63
+ queue_time -= network_time
64
+
65
+ @queue_time = (queue_time > 0) ? queue_time : 0
66
+ end
67
+
68
+ def started_at
69
+ if request_start
70
+ # There are several variants of this header. We handle these:
71
+ # - whole milliseconds (Heroku)
72
+ # - whole microseconds (???)
73
+ # - whole nanoseconds (Render)
74
+ # - fractional seconds (NGINX)
75
+ # - preceeding "t=" (NGINX)
76
+ value = request_start.gsub(/[^0-9.]/, "").to_f
77
+
78
+ # `value` could be seconds, milliseconds, microseconds or nanoseconds.
79
+ # We use some arbitrary cutoffs to determine which one it is.
80
+
81
+ if value > NANOSECONDS_CUTOFF
82
+ Time.at(value / 1_000_000_000.0)
83
+ elsif value > MICROSECONDS_CUTOFF
84
+ Time.at(value / 1_000_000.0)
85
+ elsif value > MILLISECONDS_CUTOFF
86
+ Time.at(value / 1000.0)
87
+ else
88
+ Time.at(value)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,7 @@
1
+ module RequestQueueTime
2
+ class Railtie < Rails::Railtie
3
+ initializer "RequestQueueTime.request_middleware" do |app|
4
+ app.middleware.insert_before 0, RequestQueueTime::Middleware
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ require "request_queue_time/middleware"
2
+ require "services/auto_scaling_metrics"
3
+
4
+ module RequestQueueTime
5
+ end
6
+
7
+ if defined?(Rails)
8
+ require "request_queue_time/railtie"
9
+ end
@@ -0,0 +1,112 @@
1
+ # Copyright 2024 Judoscale
2
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
3
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
4
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
5
+
6
+ require "aws-sdk-cloudwatch"
7
+ require "singleton"
8
+
9
+ module RequestQueueTime
10
+ module AutoScalingMetrics
11
+ class Reporter
12
+ include Singleton
13
+
14
+ DIMENSIONS = [
15
+ {name: "service", value: ENV["DD_SERVICE"]},
16
+ {name: "environment", value: ENV["DD_ENV"]}
17
+ ]
18
+
19
+ attr_accessor :collector
20
+
21
+ def self.start(&)
22
+ return false if instance.started?
23
+
24
+ instance.start!(&)
25
+ end
26
+
27
+ def self.stop
28
+ return unless instance.started?
29
+ instance.stop!
30
+ end
31
+
32
+ def start!(&block)
33
+ Rails.logger.info "Starting AutoScalingMetrics::Reporter"
34
+ @interval = 30
35
+ @buffer = Queue.new
36
+ @buffer_size_limit = 1000
37
+ @cloudwatch = Aws::CloudWatch::Client.new
38
+ @last_flush_time = Time.now
39
+ @pid = Process.pid
40
+
41
+ yield self if block
42
+
43
+ start_flush_thread
44
+ end
45
+
46
+ def started?
47
+ @pid == Process.pid
48
+ end
49
+
50
+ def stop!
51
+ @_thread&.terminate
52
+ @_thread = nil
53
+ @pid = nil
54
+ end
55
+
56
+ def track_request_queue_time(time)
57
+ add_metric(metric_name: "request_queue_time", value: time, unit: "Milliseconds", timestamp: Time.now)
58
+ end
59
+
60
+ def self.add_metric(metric)
61
+ instance.add_metric(metric)
62
+ end
63
+
64
+ def add_metric(metric)
65
+ metric[:dimensions] = (metric[:dimensions] || []) + DIMENSIONS
66
+ @buffer << metric
67
+ end
68
+
69
+ private
70
+
71
+ def start_flush_thread
72
+ @_thread = Thread.new do
73
+ Thread.current.name = "auto_scaling_metrics.#{@pid}"
74
+
75
+ loop do
76
+ if should_flush?
77
+ collector&.call
78
+ flush_metrics
79
+ end
80
+ rescue => ex
81
+ Sentry.capture_exception(ex)
82
+ ensure
83
+ sleep(1)
84
+ end
85
+ end
86
+ end
87
+
88
+ def should_flush?
89
+ @buffer.size >= @buffer_size_limit || (Time.now - @last_flush_time) >= @interval
90
+ end
91
+
92
+ def flush_metrics
93
+ metrics_to_flush = []
94
+ metrics_to_flush << @buffer.pop until @buffer.empty?
95
+
96
+ unless metrics_to_flush.empty?
97
+ put_metrics_to_cloudwatch(metrics_to_flush)
98
+ @last_flush_time = Time.now
99
+ end
100
+ end
101
+
102
+ def put_metrics_to_cloudwatch(metrics)
103
+ return if Rails.env.development?
104
+
105
+ @cloudwatch.put_metric_data(
106
+ namespace: "Teamtailor/queue_times",
107
+ metric_data: metrics.take(1000)
108
+ )
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,26 @@
1
+ module RequestQueueTime
2
+ module AutoScalingMetrics
3
+ class SidekiqReporter
4
+ def self.enable
5
+ Sidekiq.configure_server do |config|
6
+ config.on(:leader) do
7
+ AutoScalingMetrics::Reporter.start do |reporter|
8
+ reporter.collector = method(:collect_metrics)
9
+ end
10
+ end
11
+ end
12
+ end
13
+
14
+ def self.collect_metrics
15
+ Sidekiq::Queue.all.each do |queue|
16
+ AutoScalingMetrics::Reporter.add_metric(
17
+ metric_name: "sidekiq_queue_latency",
18
+ value: queue.paused? ? 0 : queue.latency,
19
+ unit: "Seconds",
20
+ dimensions: [{name: "queue_name", value: queue.name}]
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ require_relative "auto_scaling_metrics/reporter"
2
+ require_relative "auto_scaling_metrics/sidekiq_reporter"
3
+
4
+ module RequestQueueTime
5
+ module AutoScalingMetrics
6
+ end
7
+ end
@@ -0,0 +1,29 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "request_queue_time/middleware/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "request_queue_time"
7
+ spec.version = RequestQueueTime::Middleware::VERSION
8
+ spec.authors = ["Jonas Brusman", "Björn Nordstrand"]
9
+ spec.homepage = "https://github.com/teamtailor/request_queue_time"
10
+ spec.summary = "Used by ECS stacks for Teamtailor"
11
+
12
+ spec.metadata["homepage_uri"] = spec.homepage
13
+ spec.metadata["source_code_uri"] = "https://github.com/teamtailor/request_queue_time"
14
+ spec.metadata["changelog_uri"] = "https://github.com/teamtailor/request_queue_time/blob/main/CHANGELOG.md"
15
+ spec.metadata["github_repo"] = "ssh://github.com/teamtailor/request_queue_time"
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ end
22
+
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_dependency "aws-sdk-cloudwatch"
26
+ spec.add_dependency "dogstatsd-ruby"
27
+
28
+ spec.add_development_dependency "bundler", "~> 2.4"
29
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: request_queue_time
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Jonas Brusman
8
+ - Björn Nordstrand
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2025-02-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: aws-sdk-cloudwatch
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: dogstatsd-ruby
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: bundler
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '2.4'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '2.4'
56
+ description:
57
+ email:
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".env.test"
63
+ - ".github/workflows/main.yml"
64
+ - ".gitignore"
65
+ - ".rspec"
66
+ - ".ruby-version"
67
+ - ".vscode/settings.json"
68
+ - Gemfile
69
+ - Gemfile.lock
70
+ - README.md
71
+ - Rakefile
72
+ - bin/console
73
+ - bin/setup
74
+ - lib/request_queue_time.rb
75
+ - lib/request_queue_time/middleware.rb
76
+ - lib/request_queue_time/middleware/version.rb
77
+ - lib/request_queue_time/railtie.rb
78
+ - lib/services/auto_scaling_metrics.rb
79
+ - lib/services/auto_scaling_metrics/reporter.rb
80
+ - lib/services/auto_scaling_metrics/sidekiq_reporter.rb
81
+ - request_queue_time.gemspec
82
+ homepage: https://github.com/teamtailor/request_queue_time
83
+ licenses: []
84
+ metadata:
85
+ homepage_uri: https://github.com/teamtailor/request_queue_time
86
+ source_code_uri: https://github.com/teamtailor/request_queue_time
87
+ changelog_uri: https://github.com/teamtailor/request_queue_time/blob/main/CHANGELOG.md
88
+ github_repo: ssh://github.com/teamtailor/request_queue_time
89
+ post_install_message:
90
+ rdoc_options: []
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubygems_version: 3.5.22
105
+ signing_key:
106
+ specification_version: 4
107
+ summary: Used by ECS stacks for Teamtailor
108
+ test_files: []