Add unicorn worker worker instrumentation (#65)

approved
#1

Add unicorn worker worker instrumentation (#65)

diff --git a/README.md b/README.md
index 4855e03..2749510 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ To learn more see [Instrumenting Rails with Prometheus](https://samsaffron.com/a
     * [Delayed Job plugin](#delayed-job-plugin)
     * [Hutch metrics](#hutch-message-processing-tracer)
   * [Puma metrics](#puma-metrics)
+  * [Unicorn metrics](#unicorn-metrics)
   * [Custom type collectors](#custom-type-collectors)
   * [Multi process mode with custom collector](#multi-process-mode-with-custom-collector)
   * [GraphQL support](#graphql-support)
@@ -158,6 +159,7 @@ gem 'prometheus_exporter'
 `‍``
 
 In an initializer:
+
 `‍``ruby
 unless Rails.env == "test"
   require 'prometheus_exporter/middleware'
@@ -275,6 +277,21 @@ after_worker_boot do
 end
 `‍``
 
+### 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)
+
+Then, run `prometheus_exporter` with `--unicorn-master` and `--unicorn-listen-address` options:
+
+`‍``bash
+prometheus_exporter --unicorn-master /var/run/unicorn.pid --unicorn-listen-address 127.0.0.1:3000
+
+# alternatively, if you're using unix sockets:
+prometheus_exporter --unicorn-master /var/run/unicorn.pid --unicorn-listen-address /var/run/unicorn.sock
+`‍``
+
+Note: You must install the `raindrops` gem in your `Gemfile` or locally.
+
 ### Custom type collectors
 
 In some cases you may have custom metrics you want to ship the collector in a batch. In this case you may still be interested in the base collector behavior, but would like to add your own special messages.
@@ -313,7 +330,6 @@ PrometheusExporter::Client.default.send_json(type: "person", age: 40)
 
 To load the custom collector run:
 
-
 `‍``
 $ bundle exec prometheus_exporter -a person_collector.rb
 `‍``
diff --git a/bin/prometheus_exporter b/bin/prometheus_exporter
index be66400..fbd78d2 100755
--- a/bin/prometheus_exporter
+++ b/bin/prometheus_exporter
@@ -37,6 +37,13 @@ def run
       options[:verbose] = true
     end
 
+    opt.on('--unicorn-listen-address ADDRESS', String, '(optional) Address where unicorn listens on (unix or TCP address)') do |o|
+      options[:unicorn_listen_address] = o
+    end
+
+    opt.on('--unicorn-master PID_FILE', String, '(optional) PID file of unicorn master process to monitor unicorn') do |o|
+      options[:unicorn_pid_file] = o
+    end
   end.parse!
 
   if custom_collector_filename
diff --git a/lib/prometheus_exporter/instrumentation.rb b/lib/prometheus_exporter/instrumentation.rb
index 1c3a484..7e18447 100644
--- a/lib/prometheus_exporter/instrumentation.rb
+++ b/lib/prometheus_exporter/instrumentation.rb
@@ -4,3 +4,4 @@ require_relative "instrumentation/sidekiq"
 require_relative "instrumentation/delayed_job"
 require_relative "instrumentation/puma"
 require_relative "instrumentation/hutch"
+require_relative "instrumentation/unicorn"
diff --git a/lib/prometheus_exporter/instrumentation/unicorn.rb b/lib/prometheus_exporter/instrumentation/unicorn.rb
new file mode 100644
index 0000000..d0b5413
--- /dev/null
+++ b/lib/prometheus_exporter/instrumentation/unicorn.rb
@@ -0,0 +1,68 @@
+begin
+  require 'raindrops'
+rescue LoadError
+  # No raindrops available, dont do anything
+end
+
+module PrometheusExporter::Instrumentation
+  # collects stats from unicorn
+  class Unicorn
+    def self.start(pid_file:, listener_address:, client:, frequency: 30)
+      unicorn_collector = new(pid_file: pid_file, listener_address: listener_address)
+      client ||= PrometheusExporter::Client.default
+      Thread.new do
+        loop do
+          metric = unicorn_collector.collect
+          client.send_json metric
+        rescue StandardError => e
+          STDERR.puts("Prometheus Exporter Failed To Collect Unicorn Stats #{e}")
+        ensure
+          sleep frequency
+        end
+      end
+    end
+
+    def initialize(pid_file:, listener_address:)
+      @pid_file = pid_file
+      @listener_address = listener_address
+      @tcp = listener_address =~ /\A.+:\d+\z/
+    end
+
+    def collect
+      metric = {}
+      metric[:type] = 'unicorn'
+      collect_unicorn_stats(metric)
+      metric
+    end
+
+    def collect_unicorn_stats(metric)
+      stats = listener_address_stats
+
+      metric[:active_workers_total] = stats.active
+      metric[:request_backlog_total] = stats.queued
+      metric[:workers_total] = worker_process_count
+    end
+
+    private
+
+    def worker_process_count
+      return nil unless File.exist?(@pid_file)
+      pid = File.read(@pid_file)
+
+      return nil unless pid && pid.to_i > 0
+
+      # find all processes whose parent is the unicorn master
+      # but we're actually only interested in the number of processes (= lines of output)
+      result = `ps --no-header -o pid --ppid #{pid}`
+      result.lines.count
+    end
+
+    def listener_address_stats
+      if @tcp
+        Raindrops::Linux.tcp_listener_stats(@listener_address)[@listener_address]
+      else
+        Raindrops::Linux.unix_listener_stats(@listener_address)[@listener_address]
+      end
+    end
+  end
+end
diff --git a/lib/prometheus_exporter/server.rb b/lib/prometheus_exporter/server.rb
index 48b03a4..f10e923 100644
--- a/lib/prometheus_exporter/server.rb
+++ b/lib/prometheus_exporter/server.rb
@@ -10,3 +10,4 @@ require_relative "server/web_server"
 require_relative "server/runner"
 require_relative "server/puma_collector"
 require_relative "server/hutch_collector"
+require_relative "server/unicorn_collector"
diff --git a/lib/prometheus_exporter/server/collector.rb b/lib/prometheus_exporter/server/collector.rb
index 969d797..6dfe5b7 100644
--- a/lib/prometheus_exporter/server/collector.rb
+++ b/lib/prometheus_exporter/server/collector.rb
@@ -16,6 +16,7 @@ module PrometheusExporter::Server
       register_collector(DelayedJobCollector.new)
       register_collector(PumaCollector.new)
       register_collector(HutchCollector.new)
+      register_collector(UnicornCollector.new)
     end
 
     def register_collector(collector)
diff --git a/lib/prometheus_exporter/server/runner.rb b/lib/prometheus_exporter/server/runner.rb
index ed20437..320503f 100644
--- a/lib/prometheus_exporter/server/runner.rb
+++ b/lib/prometheus_exporter/server/runner.rb
@@ -1,4 +1,6 @@
 # frozen_string_literal: true
+require 'prometheus_exporter/client'
+require_relative '../instrumentation/unicorn'
 
 module PrometheusExporter::Server
   class RunnerException < StandardError; end;
@@ -20,6 +22,15 @@ module PrometheusExporter::Server
         raise WrongInheritance, 'Collector class must be inherited from PrometheusExporter::Server::CollectorBase'
       end
 
+      if unicorn_listen_address && unicorn_pid_file
+        local_client = PrometheusExporter::LocalClient.new(collector: collector)
+        PrometheusExporter::Instrumentation::Unicorn.start(
+          pid_file: unicorn_pid_file,
+          listener_address: unicorn_listen_address,
+          client: local_client
+        )
+      end
+
       server = server_class.new port: port, collector: collector, timeout: timeout, verbose: verbose
       server.start
     end
@@ -81,6 +92,8 @@ module PrometheusExporter::Server
       @server_class || PrometheusExporter::Server::WebServer
     end
 
+    attr_accessor :unicorn_listen_address, :unicorn_pid_file
+
     def collector
       @_collector ||= collector_class.new
     end
diff --git a/lib/prometheus_exporter/server/unicorn_collector.rb b/lib/prometheus_exporter/server/unicorn_collector.rb
new file mode 100644
index 0000000..e24177c
--- /dev/null
+++ b/lib/prometheus_exporter/server/unicorn_collector.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+# custom type collector for prometheus_exporter for handling the metrics sent from

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

GitHub sha: 6721babc

Approved #2