sidekiq_queue_metrics 1.0 → 2.0

Sign up to get free protection for your applications and to get access to all the features.
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