Add support for Resque (#170)

Add support for Resque (#170)

diff --git a/README.md b/README.md
index 2c7338d..50cd6aa 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,7 @@ To learn more see [Instrumenting Rails with Prometheus](https://samsaffron.com/a
     * [Hutch metrics](#hutch-message-processing-tracer)
   * [Puma metrics](#puma-metrics)
   * [Unicorn metrics](#unicorn-process-metrics)
+  * [Resque metrics](#resque-metrics)
   * [Custom type collectors](#custom-type-collectors)
   * [Multi process mode with custom collector](#multi-process-mode-with-custom-collector)
   * [GraphQL support](#graphql-support)
@@ -539,6 +540,28 @@ end
 
 All metrics may have a `phase` label.
 
+### Resque metrics
+
+The resque metrics are using the `Resque.info` method, which queries Redis internally. To start monitoring your resque
+installation, you'll need to start the instrumentation:
+
+`‍``ruby
+# e.g. config/initializers/resque.rb
+require 'prometheus_exporter/instrumentation'
+PrometheusExporter::Instrumentation::Resque.start
+`‍``
+
+#### Metrics collected by Resque Instrumentation
+
+| Type  | Name                   | Description                            |
+| ---   | ---                    | ---                                    |
+| Gauge | `processed_jobs_total` | Total number of processed Resque jobs  |
+| Gauge | `failed_jobs_total`    | Total number of failed Resque jobs     |
+| Gauge | `pending_jobs_total`   | Total number of pending Resque jobs    |
+| Gauge | `queues_total`         | Total number of Resque queues          |
+| Gauge | `workers_total`        | Total number of Resque workers running |
+| Gauge | `working_total`        | Total number of Resque workers working |
+
 ### Unicorn process metrics
 
 In order to gather metrics from unicorn processes, we use `rainbows`, which exposes `Rainbows::Linux.tcp_listener_stats` to gather information about active workers and queued requests. To start monitoring your unicorn processes, you'll need to know both the path to unicorn PID file and the listen address (`pid_file` and `listen` in your unicorn config file)
diff --git a/lib/prometheus_exporter/instrumentation.rb b/lib/prometheus_exporter/instrumentation.rb
index 420ed51..8e52439 100644
--- a/lib/prometheus_exporter/instrumentation.rb
+++ b/lib/prometheus_exporter/instrumentation.rb
@@ -11,3 +11,4 @@ require_relative "instrumentation/hutch"
 require_relative "instrumentation/unicorn"
 require_relative "instrumentation/active_record"
 require_relative "instrumentation/shoryuken"
+require_relative "instrumentation/resque"
diff --git a/lib/prometheus_exporter/instrumentation/resque.rb b/lib/prometheus_exporter/instrumentation/resque.rb
new file mode 100644
index 0000000..89ca354
--- /dev/null
+++ b/lib/prometheus_exporter/instrumentation/resque.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+# collects stats from resque
+module PrometheusExporter::Instrumentation
+  class Resque
+    def self.start(client: nil, frequency: 30)
+      resque_collector = new
+      client ||= PrometheusExporter::Client.default
+      Thread.new do
+        while true
+          begin
+            client.send_json(resque_collector.collect)
+          rescue => e
+            STDERR.puts("Prometheus Exporter Failed To Collect Resque Stats #{e}")
+          ensure
+            sleep frequency
+          end
+        end
+      end
+    end
+
+    def collect
+      metric = {}
+      metric[:type] = "resque"
+      collect_resque_stats(metric)
+      metric
+    end
+
+    def collect_resque_stats(metric)
+      info = ::Resque.info
+
+      metric[:processed_jobs_total] = info[:processed]
+      metric[:failed_jobs_total] = info[:failed]
+      metric[:pending_jobs_total] = info[:pending]
+      metric[:queues_total] = info[:queues]
+      metric[:worker_total] = info[:workers]
+      metric[:working_total] = info[:working]
+    end
+  end
+end
diff --git a/lib/prometheus_exporter/server.rb b/lib/prometheus_exporter/server.rb
index 75b4e6a..26301c6 100644
--- a/lib/prometheus_exporter/server.rb
+++ b/lib/prometheus_exporter/server.rb
@@ -16,3 +16,4 @@ require_relative "server/hutch_collector"
 require_relative "server/unicorn_collector"
 require_relative "server/active_record_collector"
 require_relative "server/shoryuken_collector"
+require_relative "server/resque_collector"
diff --git a/lib/prometheus_exporter/server/collector.rb b/lib/prometheus_exporter/server/collector.rb
index 93755da..28c5517 100644
--- a/lib/prometheus_exporter/server/collector.rb
+++ b/lib/prometheus_exporter/server/collector.rb
@@ -20,6 +20,7 @@ module PrometheusExporter::Server
       register_collector(UnicornCollector.new)
       register_collector(ActiveRecordCollector.new)
       register_collector(ShoryukenCollector.new)
+      register_collector(ResqueCollector.new)
     end
 
     def register_collector(collector)
diff --git a/lib/prometheus_exporter/server/resque_collector.rb b/lib/prometheus_exporter/server/resque_collector.rb
new file mode 100644
index 0000000..bdd55b9
--- /dev/null
+++ b/lib/prometheus_exporter/server/resque_collector.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module PrometheusExporter::Server
+  class ResqueCollector < TypeCollector
+    MAX_RESQUE_METRIC_AGE = 30
+    RESQUE_GAUGES = {
+      processed_jobs_total: "Total number of processed Resque jobs.",
+      failed_jobs_total: "Total number of failed Resque jobs.",
+      pending_jobs_total: "Total number of pending Resque jobs.",
+      queues_total: "Total number of Resque queues.",
+      workers_total: "Total number of Resque workers running.",
+      working_total: "Total number of Resque workers working."
+    }
+
+    def initialize
+      @resque_metrics = []
+      @gauges = {}
+    end
+
+    def type
+      "resque"
+    end
+
+    def metrics
+      return [] if resque_metrics.length == 0
+
+      resque_metrics.map do |metric|
+        labels = metric.fetch("custom_labels", {})
+
+        RESQUE_GAUGES.map do |name, help|
+          name = name.to_s
+          if value = metric[name]
+            gauge = gauges[name] ||= PrometheusExporter::Metric::Gauge.new("resque_#{name}", help)
+            gauge.observe(value, labels)
+          end
+        end
+      end
+
+      gauges.values
+    end
+
+    def collect(object)
+      now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
+
+      object["created_at"] = now
+      resque_metrics.delete_if { |metric| metric["created_at"] + MAX_RESQUE_METRIC_AGE < now }
+      resque_metrics << object
+    end
+
+    private
+
+    attr_reader :resque_metrics, :gauges
+  end
+end
diff --git a/test/server/collector_test.rb b/test/server/collector_test.rb
index 05dc14c..167aede 100644
--- a/test/server/collector_test.rb
+++ b/test/server/collector_test.rb
@@ -489,4 +489,28 @@ class PrometheusCollectorTest < Minitest::Test
     assert(result.include?('puma_thread_pool_capacity_total{phase="0",service="service1"} 32'), "has pool capacity")
     mock_puma.verify
   end
+
+  def test_it_can_collect_resque_metrics
+    collector = PrometheusExporter::Server::Collector.new
+    client = PipedClient.new(collector, custom_labels: { service: 'service1' })
+
+    mock_resque = Minitest::Mock.new
+    mock_resque.expect(
+      :info,
+      { processed: 12, failed: 2, pending: 42, queues: 2, workers: 1, working: 1 }
+    )
+
+    instrument = PrometheusExporter::Instrumentation::Resque.new
+
+    Object.stub_const(:Resque, mock_resque) do
+      metric = instrument.collect
+      client.send_json metric
+    end
+
+    result = collector.prometheus_metrics_text
+    assert(result.include?('resque_processed_jobs_total{service="service1"} 12'), "has processed jobs")
+    assert(result.include?('resque_failed_jobs_total{service="service1"} 2'), "has failed jobs")
+    assert(result.include?('resque_pending_jobs_total{service="service1"} 42'), "has pending jobs")
+    mock_resque.verify
+  end
 end
diff --git a/test/server/resque_collector_test.rb b/test/server/resque_collector_test.rb

[... diff too long, it was truncated ...]

GitHub sha: 767b6a4e

This commit appears in #170 which was merged by SamSaffron.