strongmind-sidekiq-cloudwatchmetrics 2.6.1

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: 2534e200d85aa68afbeefe1bad73d763957238a4e4054836d5a87e8c2f6a785f
4
+ data.tar.gz: 18fc7cb9633a664c2b6fb461cfb98009ad84e5509516b46487b621d003d411c6
5
+ SHA512:
6
+ metadata.gz: b3554341292c7f699cf5d29a388a4d855da613ddff1adad7d1fe540f97c3fa023e6730ff79ffd9ca335abb0f2b9c538c353da1692b61843fb7849963be243194
7
+ data.tar.gz: d57f5f73e990f75a05094865e11b5dc8cbda856262991e40d7c918535981a3500c25caea42ce346b71aa8822ae3c3e95971ea25986742ff89fee0ab68b42ccd2
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Samuel Cochran
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # Sidekiq CloudWatch Metrics
2
+
3
+ [![Build Status](https://travis-ci.org/sj26/sidekiq-cloudwatchmetrics.svg)](https://travis-ci.org/sj26/sidekiq-cloudwatchmetrics)
4
+
5
+ Runs a thread inside your Sidekiq processes to report metrics to CloudWatch
6
+ useful for autoscaling and keeping an eye on your queues.
7
+
8
+ Optimised for Sidekiq Enterprise with leader election, but works everywhere!
9
+
10
+ <img width="1055" alt="Screenshot of Sidekiq metrics in a CloudWatch dashboard" src="https://user-images.githubusercontent.com/14028/44190767-9fd66280-a16b-11e8-8b12-3d5e0641c15f.png">
11
+
12
+ ## Installation
13
+
14
+ Add this gem to your application’s Gemfile near sidekiq and then run `bundle install`:
15
+
16
+ ```ruby
17
+ gem "sidekiq"
18
+ gem "sidekiq-cloudwatchmetrics"
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Add near your Sidekiq configuration, like in `config/initializers/sidekiq.rb` in Rails:
24
+
25
+ ```ruby
26
+ require "sidekiq"
27
+ require "sidekiq/cloud_watch_metrics"
28
+
29
+ Sidekiq::CloudWatchMetrics.enable!
30
+ ```
31
+
32
+ By default this assumes you're running on an EC2 instance with an instance role
33
+ that can publish CloudWatch metrics, or that you've supplied AWS credentials
34
+ through environment variables that aws-sdk expects. You can also explicitly
35
+ supply an [aws-sdk CloudWatch Client instance][cwclient]:
36
+
37
+ ```ruby
38
+ Sidekiq::CloudWatchMetrics.enable!(client: Aws::CloudWatch::Client.new)
39
+ ```
40
+
41
+ [cwclient]: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CloudWatch/Client.html
42
+
43
+ Add 'NAMESPACE' to your environment variables (this var is already available in StrongMind Rails deployments) or explicitly supply a namespace:
44
+ ```ruby
45
+ Sidekiq::CloudWatchMetrics.enable!(client: Aws::CloudWatch::Client.new, namespace: ENV.fetch('NAMESPACE'), metrics_to_publish: %w[enqueued_jobs busy_workers], interval: 60)
46
+ ```
47
+
48
+
49
+ ## Development
50
+
51
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
52
+
53
+ 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).
54
+
55
+ ## Contributing
56
+
57
+ Bug reports and pull requests are welcome on GitHub at https://github.com/StrongMind/strongmind-sidekiq-cloudwatchmetrics.
58
+
59
+ ## License
60
+
61
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
62
+
@@ -0,0 +1,274 @@
1
+
2
+
3
+ require "sidekiq"
4
+ require "sidekiq/api"
5
+ require "aws-sdk-cloudwatch"
6
+
7
+ module Sidekiq::CloudWatchMetrics
8
+ def self.enable!(**kwargs)
9
+ Sidekiq.configure_server do |config|
10
+ publisher = Publisher.new(**kwargs)
11
+
12
+ # Sidekiq enterprise has a globally unique leader thread, making it
13
+ # easier to publish the cluster-wide metrics from one place.
14
+ if defined?(Sidekiq::Enterprise)
15
+ config.on(:leader) do
16
+ publisher.start
17
+ end
18
+ else
19
+ # Otherwise pubishing from every node doesn't hurt, it's just wasteful
20
+ config.on(:startup) do
21
+ publisher.start
22
+ end
23
+ end
24
+
25
+ config.on(:quiet) do
26
+ publisher.quiet if publisher.running?
27
+ end
28
+
29
+ config.on(:shutdown) do
30
+ publisher.stop if publisher.running?
31
+ end
32
+ end
33
+ end
34
+
35
+ class Publisher
36
+ begin
37
+ require "sidekiq/util"
38
+ include Sidekiq::Util
39
+ rescue LoadError
40
+ # Sidekiq 6.5 refactored to use Sidekiq::Component
41
+ require "sidekiq/component"
42
+ include Sidekiq::Component
43
+ end
44
+
45
+ def initialize(config: Sidekiq, client: Aws::CloudWatch::Client.new, namespace: "Sidekiq", process_metrics: true, additional_dimensions: {}, metrics_to_publish: [], interval: 60)
46
+ # Sidekiq 6.5+ requires @config, which defaults to the top-level
47
+ # `Sidekiq` module, but can be overridden when running multiple Sidekiqs.
48
+ @config = config
49
+ @client = client
50
+ @namespace = namespace
51
+ @process_metrics = process_metrics
52
+ @additional_dimensions = additional_dimensions.map { |k, v| {name: k.to_s, value: v.to_s} }
53
+ @metrics_to_publish = metrics_to_publish
54
+ @interval = interval
55
+ end
56
+
57
+ def start
58
+ logger.debug { "Starting Sidekiq CloudWatch Metrics Publisher" }
59
+
60
+ @done = false
61
+ @thread = safe_thread("cloudwatch metrics publisher", &method(:run))
62
+ end
63
+
64
+ def running?
65
+ !@thread.nil? && @thread.alive?
66
+ end
67
+
68
+ def run
69
+ logger.info { "Started Sidekiq CloudWatch Metrics Publisher" }
70
+
71
+ # Publish stats every INTERVAL seconds, sleeping as required between runs
72
+ now = Time.now.to_f
73
+ tick = now
74
+ until @stop
75
+ logger.debug { "Publishing Sidekiq CloudWatch Metrics" }
76
+ publish
77
+
78
+ now = Time.now.to_f
79
+ tick = [tick + @interval, now].max
80
+ sleep(tick - now) if tick > now
81
+ end
82
+
83
+ logger.debug { "Stopped Sidekiq CloudWatch Metrics Publisher" }
84
+ end
85
+
86
+ def publish
87
+ now = Time.now
88
+ stats = Sidekiq::Stats.new
89
+ processes = Sidekiq::ProcessSet.new.to_enum(:each).to_a
90
+ queues = stats.queues
91
+
92
+ metrics = [
93
+ {
94
+ metric_name: "ProcessedJobs",
95
+ timestamp: now,
96
+ value: stats.processed,
97
+ unit: "Count",
98
+ },
99
+ {
100
+ metric_name: "FailedJobs",
101
+ timestamp: now,
102
+ value: stats.failed,
103
+ unit: "Count",
104
+ },
105
+ {
106
+ metric_name: "EnqueuedJobs",
107
+ timestamp: now,
108
+ value: stats.enqueued,
109
+ unit: "Count",
110
+ },
111
+ {
112
+ metric_name: "ScheduledJobs",
113
+ timestamp: now,
114
+ value: stats.scheduled_size,
115
+ unit: "Count",
116
+ },
117
+ {
118
+ metric_name: "RetryJobs",
119
+ timestamp: now,
120
+ value: stats.retry_size,
121
+ unit: "Count",
122
+ },
123
+ {
124
+ metric_name: "DeadJobs",
125
+ timestamp: now,
126
+ value: stats.dead_size,
127
+ unit: "Count",
128
+ },
129
+ {
130
+ metric_name: "Workers",
131
+ timestamp: now,
132
+ value: stats.workers_size,
133
+ unit: "Count",
134
+ },
135
+ {
136
+ metric_name: "Processes",
137
+ timestamp: now,
138
+ value: stats.processes_size,
139
+ unit: "Count",
140
+ },
141
+ {
142
+ metric_name: "DefaultQueueLatency",
143
+ timestamp: now,
144
+ value: stats.default_queue_latency,
145
+ unit: "Seconds",
146
+ },
147
+ {
148
+ metric_name: "Capacity",
149
+ timestamp: now,
150
+ value: calculate_capacity(processes),
151
+ unit: "Count",
152
+ },
153
+ ]
154
+
155
+ utilization = calculate_utilization(processes) * 100.0
156
+
157
+ unless utilization.nan?
158
+ metrics << {
159
+ metric_name: "Utilization",
160
+ timestamp: now,
161
+ value: utilization,
162
+ unit: "Percent",
163
+ }
164
+ end
165
+
166
+ processes.group_by do |process|
167
+ process["tag"]
168
+ end.each do |(tag, tag_processes)|
169
+ next if tag.nil?
170
+
171
+ tag_utilization = calculate_utilization(tag_processes) * 100.0
172
+
173
+ unless tag_utilization.nan?
174
+ metrics << {
175
+ metric_name: "Utilization",
176
+ dimensions: [{name: "Tag", value: tag}],
177
+ timestamp: now,
178
+ value: tag_utilization,
179
+ unit: "Percent",
180
+ }
181
+ end
182
+ end
183
+
184
+ if @process_metrics
185
+ processes.each do |process|
186
+ process_utilization = process["busy"] / process["concurrency"].to_f * 100.0
187
+
188
+ unless process_utilization.nan?
189
+ process_dimensions = [{name: "Hostname", value: process["hostname"]}]
190
+
191
+ if process["tag"]
192
+ process_dimensions << {name: "Tag", value: process["tag"]}
193
+ end
194
+
195
+ metrics << {
196
+ metric_name: "Utilization",
197
+ dimensions: process_dimensions,
198
+ timestamp: now,
199
+ value: process_utilization,
200
+ unit: "Percent",
201
+ }
202
+ end
203
+ end
204
+ end
205
+
206
+ queues.each do |(queue_name, queue_size)|
207
+ metrics << {
208
+ metric_name: "QueueSize",
209
+ dimensions: [{name: "QueueName", value: queue_name}],
210
+ timestamp: now,
211
+ value: queue_size,
212
+ unit: "Count",
213
+ }
214
+
215
+ queue_latency = Sidekiq::Queue.new(queue_name).latency
216
+
217
+ metrics << {
218
+ metric_name: "QueueLatency",
219
+ dimensions: [{name: "QueueName", value: queue_name}],
220
+ timestamp: now,
221
+ value: queue_latency,
222
+ unit: "Seconds",
223
+ }
224
+ end
225
+
226
+ unless @additional_dimensions.empty?
227
+ metrics = metrics.each do |metric|
228
+ metric[:dimensions] = (metric[:dimensions] || []) + @additional_dimensions
229
+ end
230
+ end
231
+
232
+ # We can only put 20 metrics at a time
233
+ metrics.select { |metric| @metrics_to_publish.include?(metric[:metric_name]) }.each_slice(20) do |some_metrics|
234
+ @client.put_metric_data(
235
+ namespace: @namespace,
236
+ metric_data: some_metrics,
237
+ )
238
+ end
239
+ end
240
+
241
+ # Returns the total number of workers across all processes
242
+ private def calculate_capacity(processes)
243
+ processes.map do |process|
244
+ process["concurrency"]
245
+ end.sum
246
+ end
247
+
248
+ # Returns busy / concurrency averaged across processes (for scaling)
249
+ # Avoid considering processes not yet running any threads
250
+ private def calculate_utilization(processes)
251
+ process_utilizations = processes.map do |process|
252
+ process["busy"] / process["concurrency"].to_f
253
+ end.reject(&:nan?)
254
+
255
+ process_utilizations.sum / process_utilizations.size.to_f
256
+ end
257
+
258
+ def quiet
259
+ logger.debug { "Quieting Sidekiq CloudWatch Metrics Publisher" }
260
+ @stop = true
261
+ end
262
+
263
+ def stop
264
+ logger.debug { "Stopping Sidekiq CloudWatch Metrics Publisher" }
265
+ @stop = true
266
+ @thread.wakeup
267
+ @thread.join
268
+ rescue ThreadError
269
+ # Don't raise if thread is already dead.
270
+ nil
271
+ end
272
+ end
273
+ end
274
+
metadata ADDED
@@ -0,0 +1,153 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: strongmind-sidekiq-cloudwatchmetrics
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.6.1
5
+ platform: ruby
6
+ authors:
7
+ - StrongMind Devops
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-04-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sidekiq
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '8.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '5.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '8.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: aws-sdk-cloudwatch
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.6'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.6'
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.2'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.2'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rake
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '12.3'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '12.3'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rspec
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.7'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '3.7'
89
+ - !ruby/object:Gem::Dependency
90
+ name: timecop
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.9'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.9'
103
+ - !ruby/object:Gem::Dependency
104
+ name: rexml
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '3.2'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '3.2'
117
+ description: |
118
+ Runs a thread inside your Sidekiq processes to report metrics to CloudWatch
119
+ useful for autoscaling and keeping an eye on your queues.
120
+
121
+ Optimised for Sidekiq Enterprise with leader election, but works everywhere!
122
+ email: devops@strongmind.com
123
+ executables: []
124
+ extensions: []
125
+ extra_rdoc_files: []
126
+ files:
127
+ - LICENSE
128
+ - README.md
129
+ - lib/sidekiq/cloud_watch_metrics.rb
130
+ homepage: https://github.com/StrongMind/strongmind-sidekiq-cloudwatchmetrics
131
+ licenses:
132
+ - MIT
133
+ metadata: {}
134
+ post_install_message:
135
+ rdoc_options: []
136
+ require_paths:
137
+ - lib
138
+ required_ruby_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '2.4'
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ requirements: []
149
+ rubygems_version: 3.3.5
150
+ signing_key:
151
+ specification_version: 4
152
+ summary: Publish Sidekiq metrics to AWS CloudWatch
153
+ test_files: []