FEATURE: add support for local metric collection without using HTTP

approved
#1

FEATURE: add support for local metric collection without using HTTP

Adds a class called PrometheusExporter::LocalClient that can be used to talk directly to the collector bypassing JSON API. This make delivering local metrics really fast from various instrumentations.

diff --git a/README.md b/README.md
index 7f12b05..e46af4e 100644
--- a/README.md
+++ b/README.md
@@ -56,10 +56,20 @@ Simplest way of consuming Prometheus exporter is in a single process mode.
 `‍``ruby
 require 'prometheus_exporter/server'
 
+# client allows instrumentation to send info to server
+require 'prometheus_exporter/client'
+require 'prometheus_exporter/instrumentation'
+
 # port is the port that will provide the /metrics route
 server = PrometheusExporter::Server::WebServer.new port: 12345
 server.start
 
+# wire up a default local client
+PrometheusExporter::Client.default = PrometheusExporter::LocalClient.new(collector: server.collector)
+
+# this ensures basic process instrumentation metrics are added such as RSS and Ruby metrics
+PrometheusExporter::Instrumentation::Process.start
+
 gauge = PrometheusExporter::Metric::Gauge.new("rss", "used RSS for process")
 counter = PrometheusExporter::Metric::Counter.new("web_requests", "number of web requests")
 summary = PrometheusExporter::Metric::Summary.new("page_load_time", "time it took to load page")
diff --git a/lib/prometheus_exporter/client.rb b/lib/prometheus_exporter/client.rb
index 7d16912..f7d80fa 100644
--- a/lib/prometheus_exporter/client.rb
+++ b/lib/prometheus_exporter/client.rb
@@ -3,197 +3,213 @@
 require 'socket'
 require 'thread'
 
-class PrometheusExporter::Client
+module PrometheusExporter
+
+  class Client
+    class RemoteMetric
+      def initialize(name:, help:, type:, client:)
+        @name = name
+        @help = help
+        @client = client
+        @type = type
+      end
 
-  class RemoteMetric
-    def initialize(name:, help:, type:, client:)
-      @name = name
-      @help = help
-      @client = client
-      @type = type
-    end
+      def standard_values(value, keys, prometheus_exporter_action = nil)
+        values = {
+          type: @type,
+          help: @help,
+          name: @name,
+          keys: keys,
+          value: value
+        }
+        values[:prometheus_exporter_action] = prometheus_exporter_action if prometheus_exporter_action
+        values
+      end
 
-    def standard_values(value, keys, prometheus_exporter_action = nil)
-      values = {
-        type: @type,
-        help: @help,
-        name: @name,
-        keys: keys,
-        value: value
-      }
-      values[:prometheus_exporter_action] = prometheus_exporter_action if prometheus_exporter_action
-      values
-    end
+      def observe(value = 1, keys = nil)
+        @client.send_json(standard_values(value, keys))
+      end
+
+      def increment(keys = nil, value = 1)
+        @client.send_json(standard_values(value, keys, :increment))
+      end
+
+      def decrement(keys = nil, value = 1)
+        @client.send_json(standard_values(value, keys, :decrement))
+      end
 
-    def observe(value = 1, keys = nil)
-      @client.send_json(standard_values(value, keys))
     end
 
-    def increment(keys = nil, value = 1)
-      @client.send_json(standard_values(value, keys, :increment))
+    def self.default
+      @default ||= new
     end
 
-    def decrement(keys = nil, value = 1)
-      @client.send_json(standard_values(value, keys, :decrement))
+    def self.default=(client)
+      @default = client
     end
 
-  end
+    MAX_SOCKET_AGE = 25
+    MAX_QUEUE_SIZE = 10_000
 
-  def self.default
-    @default ||= new
-  end
+    def initialize(host: 'localhost', port: PrometheusExporter::DEFAULT_PORT, max_queue_size: nil, thread_sleep: 0.5, json_serializer: nil, custom_labels: nil)
+      @metrics = []
 
-  def self.default=(client)
-    @default = client
-  end
+      @queue = Queue.new
+      @socket = nil
+      @socket_started = nil
 
-  MAX_SOCKET_AGE = 25
-  MAX_QUEUE_SIZE = 10_000
+      max_queue_size ||= MAX_QUEUE_SIZE
+      max_queue_size = max_queue_size.to_i
 
-  def initialize(host: 'localhost', port: PrometheusExporter::DEFAULT_PORT, max_queue_size: nil, thread_sleep: 0.5, json_serializer: nil, custom_labels: nil)
-    @metrics = []
+      if max_queue_size.to_i <= 0
+        raise ArgumentError, "max_queue_size must be larger than 0"
+      end
 
-    @queue = Queue.new
-    @socket = nil
-    @socket_started = nil
+      @max_queue_size = max_queue_size
+      @host = host
+      @port = port
+      @worker_thread = nil
+      @mutex = Mutex.new
+      @thread_sleep = thread_sleep
 
-    max_queue_size ||= MAX_QUEUE_SIZE
-    max_queue_size = max_queue_size.to_i
+      @json_serializer = json_serializer == :oj ? PrometheusExporter::OjCompat : JSON
 
-    if max_queue_size.to_i <= 0
-      raise ArgumentError, "max_queue_size must be larger than 0"
+      @custom_labels = custom_labels
     end
 
-    @max_queue_size = max_queue_size
-    @host = host
-    @port = port
-    @worker_thread = nil
-    @mutex = Mutex.new
-    @thread_sleep = thread_sleep
-
-    @json_serializer = json_serializer == :oj ? PrometheusExporter::OjCompat : JSON
-
-    @custom_labels = custom_labels
-  end
-
-  def custom_labels=(custom_labels)
-    @custom_labels = custom_labels
-  end
-
-  def register(type, name, help)
-    metric = RemoteMetric.new(type: type, name: name, help: help, client: self)
-    @metrics << metric
-    metric
-  end
+    def custom_labels=(custom_labels)
+      @custom_labels = custom_labels
+    end
 
-  def send_json(obj)
-    payload = @custom_labels.nil? ? obj : obj.merge(custom_labels: @custom_labels)
-    send(@json_serializer.dump(payload))
-  end
+    def register(type, name, help)
+      metric = RemoteMetric.new(type: type, name: name, help: help, client: self)
+      @metrics << metric
+      metric
+    end
 
-  def send(str)
-    @queue << str
-    if @queue.length > @max_queue_size
-      STDERR.puts "Prometheus Exporter client is dropping message cause queue is full"
-      @queue.pop
+    def send_json(obj)
+      payload = @custom_labels.nil? ? obj : obj.merge(custom_labels: @custom_labels)
+      send(@json_serializer.dump(payload))
     end
 
-    ensure_worker_thread!
-  end
+    def send(str)
+      @queue << str
+      if @queue.length > @max_queue_size
+        STDERR.puts "Prometheus Exporter client is dropping message cause queue is full"
+        @queue.pop
+      end
 
-  def process_queue
-    while @queue.length > 0
-      ensure_socket!
+      ensure_worker_thread!
+    end
 
-      begin
-        message = @queue.pop
-        @socket.write(message.bytesize.to_s(16).upcase)
-        @socket.write("\r\n")
-        @socket.write(message)
-        @socket.write("\r\n")
-      rescue => e
-        STDERR.puts "Prometheus Exporter is dropping a message: #{e}"
-        @socket = nil
-        raise
+    def process_queue
+      while @queue.length > 0
+        ensure_socket!
+
+        begin
+          message = @queue.pop
+          @socket.write(message.bytesize.to_s(16).upcase)
+          @socket.write("\r\n")
+          @socket.write(message)
+          @socket.write("\r\n")
+        rescue => e
+          STDERR.puts "Prometheus Exporter is dropping a message: #{e}"
+          @socket = nil
+          raise
+        end
       end
     end
-  end
 
-  def stop
-    @mutex.synchronize do
-      @worker_thread&.kill
-      while @worker_thread.alive?
-        sleep 0.001
+    def stop
+      @mutex.synchronize do
+        @worker_thread&.kill
+        while @worker_thread.alive?
+          sleep 0.001
+        end
+        @worker_thread = nil
       end
-      @worker_thread = nil
-    end
 
-    close_socket!
-  end
+      close_socket!
+    end
 
-  private
+    private
 
-  def worker_loop
-    close_socket_if_old!
-    process_queue
-  rescue => e
-    STDERR.puts "Prometheus Exporter, failed to send message #{e}"
-  end
+    def worker_loop
+      close_socket_if_old!
+      process_queue
+    rescue => e
+      STDERR.puts "Prometheus Exporter, failed to send message #{e}"
+    end
 
-  def ensure_worker_thread!
-    unless @worker_thread&.alive?
-      @mutex.synchronize do
-        return if @worker_thread&.alive?
+    def ensure_worker_thread!

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

GitHub sha: b3faf90d

Approved #2