sidekiq_queue_metrics 1.0 → 2.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: 616f7d90c5c36572b23161d8fcc09fdac5e05aa3fc4a4770445ecadd916854fa
4
- data.tar.gz: e7ef6953e73d4c64a7bab64a3d6302be8f9aebd51152667be1815f76956d1a1c
3
+ metadata.gz: 8d104466f165a5787ee101d1a4d4b7e1909d47dafab3f7ea3363454f44a22f27
4
+ data.tar.gz: 6eeee50fdc2eedb29aa6adc216b1ede0249b57e49770638fb96300f0400c7b0f
5
5
  SHA512:
6
- metadata.gz: 5457ea650cfb8a27175bd07a480695ac8105d4a31f4115a6fd2fd17b6b9e1fd4640deb332b94818eae24e24e487c4e7c4308839a3be776852d5e2bd6bda22284
7
- data.tar.gz: 8a0425708609f2a8d0887b695261c2b9d354c038952cb597c26163c6f92e050aed980c0981c6667b4469c40dc3cc26c5518b3a02a229e7f1193bccc83f82f8b6
6
+ metadata.gz: bd32c75be538976be8659d35016c2d5eaabc36cabd33f2ddf351f4dd3289bd758d67e16f8b9fe912ca615c7167b9b8d683a4ae3278bbf6571f22d21f1fe0f673
7
+ data.tar.gz: 8916f90cc120083e3c66a0553b2b6eda9273bcc8a47148c000a7fd981f86f174e3d03094be9f0a8e2c9b0b410917bdf15bf0e3297722ecc561f56d79464492ba
data/README.md CHANGED
@@ -37,8 +37,8 @@ Sidekiq::QueueMetrics.fetch
37
37
  Output:
38
38
  ```ruby
39
39
  {
40
- "mailer_queue" => {"processed" => 5, "failed" => 1, "enqueued" => 2, "in_retry" => 0},
41
- "default_queue" => {"processed" => 10, "failed" => 0, "enqueued" => 1, "in_retry" => 1}
40
+ "mailer_queue" => {"processed" => 5, "failed" => 1, "enqueued" => 2, "in_retry" => 0, "scheduled" => 0},
41
+ "default_queue" => {"processed" => 10, "failed" => 0, "enqueued" => 1, "in_retry" => 1, "scheduled" => 2}
42
42
  }
43
43
  ```
44
44
 
@@ -11,6 +11,14 @@ module Sidekiq::QueueMetrics
11
11
  @storage_location = key
12
12
  end
13
13
 
14
+ def self.max_recently_failed_jobs=(count)
15
+ @max_recently_failed_jobs = count
16
+ end
17
+
18
+ def self.max_recently_failed_jobs
19
+ @max_recently_failed_jobs || 50
20
+ end
21
+
14
22
  def self.storage_location
15
23
  @storage_location
16
24
  end
@@ -4,11 +4,15 @@ module Sidekiq::QueueMetrics
4
4
  class JobDeathMonitor < Monitor
5
5
  def self.proc
6
6
  Proc.new do |job, exception|
7
- queue = job['queue']
8
- JobDeathMonitor.new.monitor(queue)
7
+ JobDeathMonitor.new.monitor(job)
9
8
  end
10
9
  end
11
10
 
11
+ def monitor(job)
12
+ super(job['queue'])
13
+ Storage.add_failed_job(job)
14
+ end
15
+
12
16
  def status_counter
13
17
  'failed'
14
18
  end
@@ -46,6 +46,10 @@ module Sidekiq::QueueMetrics
46
46
  Sidekiq::ScheduledSet.new.group_by(&:queue).map {|queue, jobs| [queue, jobs.count]}.to_h
47
47
  end
48
48
 
49
+ def failed_jobs(queue)
50
+ Storage.failed_jobs(queue).reverse
51
+ end
52
+
49
53
  private
50
54
  def val_or_default(val, default = 0)
51
55
  val || default
@@ -1,5 +1,7 @@
1
1
  module Sidekiq::QueueMetrics
2
2
  class Storage
3
+ FAILED_JOBS_KEY = 'failed_jobs'.freeze
4
+
3
5
  class << self
4
6
  def set_stats(key = stats_key, value)
5
7
  Sidekiq.redis_pool.with do |conn|
@@ -13,6 +15,25 @@ module Sidekiq::QueueMetrics
13
15
  end
14
16
  end
15
17
 
18
+ def add_failed_job(job, max_count = Sidekiq::QueueMetrics.max_recently_failed_jobs)
19
+ Sidekiq.redis_pool.with do |conn|
20
+ queue = job['queue']
21
+ failed_jobs = JSON.parse(conn.get("#{FAILED_JOBS_KEY}:#{queue}") || '[]')
22
+
23
+ if failed_jobs.size >= max_count
24
+ (failed_jobs.size - max_count + 1).times {failed_jobs.shift}
25
+ end
26
+
27
+ conn.set("#{FAILED_JOBS_KEY}:#{queue}", (failed_jobs << job).to_json)
28
+ end
29
+ end
30
+
31
+ def failed_jobs(queue)
32
+ Sidekiq.redis_pool.with do |conn|
33
+ JSON.parse(conn.get("#{FAILED_JOBS_KEY}:#{queue}") || '[]')
34
+ end
35
+ end
36
+
16
37
  def stats_key
17
38
  Sidekiq::QueueMetrics.storage_location || 'queue_stats'
18
39
  end
@@ -1,5 +1,5 @@
1
1
  module Sidekiq
2
2
  module QueueMetrics
3
- VERSION = '1.0'
3
+ VERSION = '2.0'
4
4
  end
5
5
  end
@@ -0,0 +1,80 @@
1
+ <header>
2
+ <h3><%= t('Job') %></h3>
3
+ </header>
4
+
5
+ <div class="table_container">
6
+ <table class="table table-bordered table-striped">
7
+ <tbody>
8
+ <tr>
9
+ <th><%= t('Queue') %></th>
10
+ <td>
11
+ <%= @job['queue'] %>
12
+ </td>
13
+ </tr>
14
+ <tr>
15
+ <th><%= t('Job') %></th>
16
+ <td>
17
+ <code><%= @job['class'] %></code>
18
+ </td>
19
+ </tr>
20
+ <tr>
21
+ <th><%= t('Arguments') %></th>
22
+ <td>
23
+ <code class="code-wrap">
24
+ <div class="args-extended"><%= @job['args'].join(', ') %></div>
25
+ </code>
26
+ </td>
27
+ </tr>
28
+ <tr>
29
+ <th>JID</th>
30
+ <td>
31
+ <code><%= @job['jid'] %></code>
32
+ </td>
33
+ </tr>
34
+ <tr>
35
+ <th><%= t('CreatedAt') %></th>
36
+ <td><%= relative_time(Time.at(@job['created_at'])) %></td>
37
+ </tr>
38
+ <tr>
39
+ <th><%= t('Enqueued') %></th>
40
+ <td><%= relative_time(Time.at(@job['enqueued_at'])) %></td>
41
+ </tr>
42
+ <% unless retry_extra_items(OpenStruct.new(item: @job)).empty? %>
43
+ <tr>
44
+ <th><%= t('Extras') %></th>
45
+ <td>
46
+ <code>
47
+ <%= retry_extra_items(OpenStruct.new(item: @job)).inspect %>
48
+ </code>
49
+ </td>
50
+ </tr>
51
+ <% end %>
52
+ </tbody>
53
+ </table>
54
+
55
+ <h3><%= t('Error') %></h3>
56
+ <div class="table_container">
57
+ <table class="error table table-bordered table-striped">
58
+ <tbody>
59
+ <tr>
60
+ <th><%= t('ErrorClass') %></th>
61
+ <td>
62
+ <code><%= @job['error_class'] %></code>
63
+ </td>
64
+ </tr>
65
+ <tr>
66
+ <th><%= t('ErrorMessage') %></th>
67
+ <td><%= h(@job['error_message']) %></td>
68
+ </tr>
69
+ <% if !@job['error_backtrace'].nil? %>
70
+ <tr>
71
+ <th><%= t('ErrorBacktrace') %></th>
72
+ <td>
73
+ <code><%= @job['error_backtrace'].join("<br/>") %></code>
74
+ </td>
75
+ </tr>
76
+ <% end %>
77
+ </tbody>
78
+ </table>
79
+ </div>
80
+ </div>
@@ -0,0 +1,70 @@
1
+ <style>
2
+ .queue_summary_bar {
3
+ padding-left: 0;
4
+ padding-right: 0;
5
+ }
6
+
7
+ .queue_summary_bar ul li {
8
+ width: 19%;
9
+ }
10
+ </style>
11
+
12
+ <header class="row">
13
+ <div class="col-sm-10">
14
+ <h3><%= @queue %></h3>
15
+ </div>
16
+ </header>
17
+
18
+ <div class="col-sm-12 summary_bar queue_summary_bar">
19
+ <ul class="list-unstyled summary row">
20
+ <li class="processed col-sm-1">
21
+ <span class="count"><%= number_with_delimiter(@queue_stats['processed']) %></span>
22
+ <span class="desc"><%= t('Processed') %></span>
23
+ </li>
24
+ <li class="failed col-sm-1">
25
+ <span class="count"><%= number_with_delimiter(@queue_stats['failed']) %></span>
26
+ <span class="desc"><%= t('Failed') %></span>
27
+ </li>
28
+ <li class="enqueued col-sm-1">
29
+ <span class="count"><%= number_with_delimiter(@queue_stats['enqueued']) %></span>
30
+ <span class="desc"><%= t('Enqueued') %></span>
31
+ </li>
32
+ <li class="retries col-sm-1">
33
+ <span class="count"><%= number_with_delimiter(@queue_stats['in_retry']) %></span>
34
+ <span class="desc"><%= t('Retries') %></span>
35
+ </li>
36
+ <li class="scheduled col-sm-1">
37
+ <span class="count"><%= number_with_delimiter(@queue_stats['scheduled']) %></span>
38
+ <span class="desc"><%= t('Scheduled') %></span>
39
+ </li>
40
+ </ul>
41
+ </div>
42
+
43
+ <header class="row">
44
+ <div class="col-sm-10">
45
+ <h3>Failed Jobs</h3>
46
+ </div>
47
+ </header>
48
+
49
+ <table class="table table-striped table-bordered table-white">
50
+ <thead>
51
+ <tr>
52
+ <th><%= t('Job') %></th>
53
+ <th><%= t('Arguments') %></th>
54
+ <th><%= t('Enqueued') %></th>
55
+ <th><%= t('Error') %></th>
56
+ </tr>
57
+ </thead>
58
+ <% @failed_jobs.each do |job| %>
59
+ <tr>
60
+ <td><%= job['class'] %></td>
61
+ <td><%= job['args'].join(', ') %></td>
62
+ <td>
63
+ <a href="<%= "/sidekiq/queue_metrics/queues/#{@queue}/jobs/#{job['jid']}" %>">
64
+ <%= relative_time(Time.at(job['enqueued_at'])) %>
65
+ </a>
66
+ </td>
67
+ <td><%= job['error_message'] %></td>
68
+ </tr>
69
+ <% end %>
70
+ </table>
@@ -38,8 +38,12 @@
38
38
  <table class="table table-striped table-bordered table-white queue_metrics">
39
39
  <thead>
40
40
  <tr>
41
- <th><span class="heading"><%= queue %></span></th>
42
- <th style="width: 30%" class="center"><span class="heading">Count</span></th>
41
+ <th>
42
+ <a href="<%= "/sidekiq/queue_metrics/queues/#{queue}/summary" %>">
43
+ <span class="heading"><%= queue %></span>
44
+ </a>
45
+ </th>
46
+ <th style="width: 30%" class="center"><span class="heading">Count</span></th>
43
47
  </tr>
44
48
  </thead>
45
49
  <% metrics.each do |metric_name, count| %>
@@ -5,7 +5,22 @@ module Sidekiq::QueueMetrics
5
5
 
6
6
  app.get "/queue_metrics" do
7
7
  @queue_metrics = Sidekiq::QueueMetrics.fetch
8
- render(:erb, File.read(File.join(view_path, "queue_metrics.erb")))
8
+ render(:erb, File.read(File.join(view_path, "queues_stats.erb")))
9
+ end
10
+
11
+ app.get '/queue_metrics/queues/:queue/summary' do
12
+ @queue = route_params[:queue]
13
+ @queue_stats = Sidekiq::QueueMetrics.fetch[@queue]
14
+ @failed_jobs = Sidekiq::QueueMetrics.failed_jobs(@queue)
15
+ render(:erb, File.read(File.join(view_path, "queue_summary.erb")))
16
+ end
17
+
18
+ app.get '/queue_metrics/queues/:queue/jobs/:jid' do
19
+ queue = route_params[:queue]
20
+ jid = route_params[:jid]
21
+ failed_jobs = Sidekiq::QueueMetrics.failed_jobs(queue)
22
+ @job = failed_jobs.find {|job| job['jid'] == jid}
23
+ render(:erb, File.read(File.join(view_path, "failed_job.erb")))
9
24
  end
10
25
  end
11
26
  end
@@ -8,6 +8,7 @@ describe Sidekiq::QueueMetrics::JobDeathMonitor do
8
8
  it 'should create stats key and add stats of queue' do
9
9
  expect(Sidekiq::QueueMetrics::Storage).to receive(:get_stats).and_return(nil)
10
10
  expect(Sidekiq::QueueMetrics::Storage).to receive(:set_stats).with({mailer_queue: {failed: 1}}.to_json)
11
+ expect(Sidekiq::QueueMetrics::Storage).to receive(:add_failed_job).with(job)
11
12
 
12
13
  monitor.call(job)
13
14
  end
@@ -15,13 +16,15 @@ describe Sidekiq::QueueMetrics::JobDeathMonitor do
15
16
 
16
17
  context 'when stats exists' do
17
18
  it 'should create a new queue when it does not exist' do
19
+ job_queue = {'queue' => 'job_queue'}
18
20
  existing_stats = {mailer_queue: {failed: 1}}.to_json
19
21
  expected_stats = {mailer_queue: {failed: 1}, job_queue: {failed: 1}}.to_json
20
22
 
21
23
  expect(Sidekiq::QueueMetrics::Storage).to receive(:get_stats).and_return(existing_stats)
22
24
  expect(Sidekiq::QueueMetrics::Storage).to receive(:set_stats).with(expected_stats)
25
+ expect(Sidekiq::QueueMetrics::Storage).to receive(:add_failed_job).with(job_queue)
23
26
 
24
- monitor.call({'queue' => 'job_queue'})
27
+ monitor.call(job_queue)
25
28
  end
26
29
 
27
30
  it 'should update existing queue' do
@@ -30,6 +33,7 @@ describe Sidekiq::QueueMetrics::JobDeathMonitor do
30
33
 
31
34
  expect(Sidekiq::QueueMetrics::Storage).to receive(:get_stats).and_return(existing_stats)
32
35
  expect(Sidekiq::QueueMetrics::Storage).to receive(:set_stats).with(expected_stats)
36
+ expect(Sidekiq::QueueMetrics::Storage).to receive(:add_failed_job).with(job)
33
37
 
34
38
  monitor.call(job)
35
39
  end
@@ -40,6 +44,7 @@ describe Sidekiq::QueueMetrics::JobDeathMonitor do
40
44
 
41
45
  expect(Sidekiq::QueueMetrics::Storage).to receive(:get_stats).and_return(existing_stats)
42
46
  expect(Sidekiq::QueueMetrics::Storage).to receive(:set_stats).with(expected_stats)
47
+ expect(Sidekiq::QueueMetrics::Storage).to receive(:add_failed_job).with(job)
43
48
 
44
49
  monitor.call(job)
45
50
  end
@@ -51,10 +51,11 @@ describe Sidekiq::QueueMetrics do
51
51
  end
52
52
 
53
53
  it 'should return Sidekiq::QueueMetrics for all sidekiq queues' do
54
- jobs_in_retry_queue = []
54
+ jobs_in_retry_queue = scheduled_jobs = []
55
55
 
56
56
  expect(Sidekiq::QueueMetrics::Storage).to receive(:get_stats).and_return(nil)
57
57
  expect(Sidekiq::RetrySet).to receive(:new).and_return(jobs_in_retry_queue)
58
+ expect(Sidekiq::ScheduledSet).to receive(:new).and_return(scheduled_jobs)
58
59
  expect(Sidekiq::Queue).to receive(:new).with('mailer_queue').and_return(OpenStruct.new(size: 0))
59
60
  expect(Sidekiq::Queue).to receive(:new).with('heavy_jobs_queue').and_return(OpenStruct.new(size: 0))
60
61
 
@@ -73,4 +74,15 @@ describe Sidekiq::QueueMetrics do
73
74
  expect(queue_stats['heavy_jobs_queue']['scheduled']).to be_zero
74
75
  end
75
76
  end
77
+
78
+ describe '#failed_jobs' do
79
+ it 'should return failed jobs for a queue' do
80
+ queue = 'default_queue'
81
+ job_1 = double(:job)
82
+ job_2 = double(:job)
83
+ expect(Sidekiq::QueueMetrics::Storage).to receive(:failed_jobs).and_return([job_1, job_2])
84
+
85
+ expect(Sidekiq::QueueMetrics.failed_jobs(queue)).to eq([job_2, job_1])
86
+ end
87
+ end
76
88
  end
@@ -0,0 +1,82 @@
1
+ describe Sidekiq::QueueMetrics::Storage do
2
+ class MockRedisPool
3
+ attr_reader :conn
4
+
5
+ def initialize(conn)
6
+ @conn = conn
7
+ end
8
+
9
+ def with
10
+ yield conn
11
+ end
12
+ end
13
+
14
+ let(:mock_redis_conn) {double(:connection)}
15
+ let(:mock_redis_pool) {MockRedisPool.new(mock_redis_conn)}
16
+
17
+ describe '#add_failed_job' do
18
+ it 'should add first failed job' do
19
+ job = {'queue' => 'mailer_queue'}
20
+ expect(Sidekiq).to receive(:redis_pool).and_return(mock_redis_pool)
21
+ expect(mock_redis_conn).to receive(:get).with("failed_jobs:mailer_queue").and_return(nil)
22
+
23
+ expect(mock_redis_conn).to receive(:set).with("failed_jobs:mailer_queue", [job].to_json)
24
+
25
+ Sidekiq::QueueMetrics::Storage.add_failed_job(job)
26
+ end
27
+
28
+ it 'should add failed job to existing jobs' do
29
+ key = "failed_jobs:mailer_queue"
30
+ new_job = {'queue' => 'mailer_queue', 'args' => [1]}
31
+ existing_jobs = [{'queue' => 'mailer_queue', 'args' => [2]}]
32
+
33
+ expect(Sidekiq).to receive(:redis_pool).and_return(mock_redis_pool)
34
+ expect(mock_redis_conn).to receive(:get).with(key).and_return(existing_jobs.to_json)
35
+
36
+ expect(mock_redis_conn).to receive(:set).with(key, [existing_jobs.first, new_job].to_json)
37
+
38
+ Sidekiq::QueueMetrics::Storage.add_failed_job(new_job)
39
+ end
40
+
41
+ it 'should delete old job when failed jobs limit has reached' do
42
+ key = "failed_jobs:mailer_queue"
43
+ new_job = {'queue' => 'mailer_queue', 'args' => [1]}
44
+ oldest_job = {'queue' => 'mailer_queue', 'args' => [2]}
45
+ older_job = {'queue' => 'mailer_queue', 'args' => [3]}
46
+
47
+ existing_jobs = [oldest_job, older_job]
48
+
49
+ expect(Sidekiq).to receive(:redis_pool).and_return(mock_redis_pool)
50
+ expect(mock_redis_conn).to receive(:get).with(key).and_return(existing_jobs.to_json)
51
+
52
+ expect(mock_redis_conn).to receive(:set).with(key, [older_job, new_job].to_json)
53
+
54
+ Sidekiq::QueueMetrics::Storage.add_failed_job(new_job, 2)
55
+ end
56
+ end
57
+
58
+ describe '#failed_jobs' do
59
+ context 'when failed jobs are not present' do
60
+ it 'should return failed jobs for a given queue' do
61
+ queue = 'mailer_queue'
62
+ expect(Sidekiq).to receive(:redis_pool).and_return(mock_redis_pool)
63
+
64
+ expect(mock_redis_conn).to receive(:get).with("failed_jobs:#{queue}").and_return(nil)
65
+
66
+ expect(Sidekiq::QueueMetrics::Storage.failed_jobs(queue)).to be_empty
67
+ end
68
+ end
69
+
70
+ context 'when failed jobs are present' do
71
+ it 'should return failed jobs for a given queue' do
72
+ queue = 'mailer_queue'
73
+ jobs = [{'queue' => 'mailer_queue', 'args' => [1]}]
74
+ expect(Sidekiq).to receive(:redis_pool).and_return(mock_redis_pool)
75
+
76
+ expect(mock_redis_conn).to receive(:get).with("failed_jobs:#{queue}").and_return(jobs.to_json)
77
+
78
+ expect(Sidekiq::QueueMetrics::Storage.failed_jobs(queue)).to eq(jobs)
79
+ end
80
+ end
81
+ end
82
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq_queue_metrics
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.0'
4
+ version: '2.0'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ajit Singh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-09-14 00:00:00.000000000 Z
11
+ date: 2018-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sidekiq
@@ -87,12 +87,15 @@ files:
87
87
  - lib/sidekiq_queue_metrics/queue_metrics.rb
88
88
  - lib/sidekiq_queue_metrics/storage.rb
89
89
  - lib/sidekiq_queue_metrics/version.rb
90
- - lib/sidekiq_queue_metrics/views/queue_metrics.erb
90
+ - lib/sidekiq_queue_metrics/views/failed_job.erb
91
+ - lib/sidekiq_queue_metrics/views/queue_summary.erb
92
+ - lib/sidekiq_queue_metrics/views/queues_stats.erb
91
93
  - lib/sidekiq_queue_metrics/web_extension.rb
92
94
  - sidekiq_queue_metrics.gemspec
93
95
  - spec/lib/sidekiq_queue_metrics/monitor/job_death_monitor_spec.rb
94
96
  - spec/lib/sidekiq_queue_metrics/monitor/job_success_monitor_spec.rb
95
- - spec/lib/sidekiq_queue_metrics/queue_stats_spec.rb
97
+ - spec/lib/sidekiq_queue_metrics/queue_metrics_spec.rb
98
+ - spec/lib/sidekiq_queue_metrics/storage_spec.rb
96
99
  - spec/spec_helper.rb
97
100
  homepage: https://github.com/ajitsing/sidekiq_queue_metrics
98
101
  licenses:
@@ -121,5 +124,6 @@ summary: Records stats of each sidekiq queue and exposes APIs to retrieve them
121
124
  test_files:
122
125
  - spec/lib/sidekiq_queue_metrics/monitor/job_death_monitor_spec.rb
123
126
  - spec/lib/sidekiq_queue_metrics/monitor/job_success_monitor_spec.rb
124
- - spec/lib/sidekiq_queue_metrics/queue_stats_spec.rb
127
+ - spec/lib/sidekiq_queue_metrics/queue_metrics_spec.rb
128
+ - spec/lib/sidekiq_queue_metrics/storage_spec.rb
125
129
  - spec/spec_helper.rb