leopard 0.2.4 → 0.2.5

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: 91aa4b92bca51e6d8b73238440961b9f3f43a0cbad93e7faa8a905ed25cf0ae0
4
- data.tar.gz: '060265234152419f9ccffae0177c83cf3a6e9d175d453fb4657163a419ce7ed9'
3
+ metadata.gz: bb81d09b4b0709f3f290009e42e9fa1080e6b33e1fa578473b789875cfa44054
4
+ data.tar.gz: 0f1d6d756f9a60cb9cd9f6a7072e6c9e2fcd1060faed0121ad6a3d27496a3ea6
5
5
  SHA512:
6
- metadata.gz: e7f3a1b44ac12f62f1bd0fd06aa9a5e48d0007f022f6664929b569e0016587d24adce6c2e3e5fb7ed769a38509d3e83e79c80e891c7470c61c6848fede6380a2
7
- data.tar.gz: 33416a919d4e8f073ea9b89a46ede9aae3979284f2614b868a997fd3e6f21dae98802755203139a55facb58c4aad16a0a5fdc42499cf7cfc04521d5eff8c3126
6
+ metadata.gz: c584384a406f264f2e3bb20c0a40fd75506e7f407985f7b4fb42166899fce3b233f99df795e6e75a4c973f6369c93cb10a9eb29f4453a4cd88c19680d9839ed2
7
+ data.tar.gz: 4bdbdfc0debc18a1bbd7ef8e636e4ad2019fa593fd083a47683f48bfb099302552e528eac530f99e464f5347c0475d1c49a874aa4021aaf5dcb2c4a4e321fe68
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.2.4"
2
+ ".": "0.2.5"
3
3
  }
data/.version.txt CHANGED
@@ -1 +1 @@
1
- 0.2.4
1
+ 0.2.5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.5](https://github.com/rubyists/leopard/compare/v0.2.4...v0.2.5) (2026-04-16)
4
+
5
+
6
+ ### Features
7
+
8
+ * add optional prometheus metrics endpoint with saturation metrics ([#42](https://github.com/rubyists/leopard/issues/42)) ([fcd767a](https://github.com/rubyists/leopard/commit/fcd767ab5438c92bf50800d50027b43b7d0f9f0e))
9
+
3
10
  ## [0.2.4](https://github.com/rubyists/leopard/compare/v0.2.3...v0.2.4) (2026-03-31)
4
11
 
5
12
 
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'erb'
5
+
6
+ module Rubyists
7
+ module Leopard
8
+ module MetricsServer
9
+ private
10
+
11
+ def start_metrics_server(workers)
12
+ port = ENV.fetch('LEOPARD_METRICS_PORT', '9394').to_i
13
+ Thread.new do
14
+ server = TCPServer.new(port)
15
+ logger.info "Metrics server listening on :#{port}"
16
+ loop { Thread.new(server.accept) { |client| handle_metrics_client(client, workers) } }
17
+ rescue StandardError => e
18
+ logger.error "Metrics server error: #{e.message}"
19
+ end
20
+ end
21
+
22
+ def handle_metrics_client(client, workers)
23
+ request_line = client.gets
24
+ loop { break if (client.gets || '').chomp.empty? }
25
+ write_metrics_response(client, request_line, workers)
26
+ rescue StandardError => e
27
+ logger.warn "Metrics request error: #{e.message}"
28
+ ensure
29
+ close_client(client)
30
+ end
31
+
32
+ def close_client(client)
33
+ client.close
34
+ rescue StandardError
35
+ nil
36
+ end
37
+
38
+ def write_metrics_response(client, request_line, workers)
39
+ if request_line&.start_with?('GET /metrics')
40
+ body = prometheus_metrics(workers)
41
+ client.write "HTTP/1.1 200 OK\r\n" \
42
+ "Content-Type: text/plain; version=0.0.4\r\n" \
43
+ "Content-Length: #{body.bytesize}\r\n\r\n#{body}"
44
+ else
45
+ client.write "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n"
46
+ end
47
+ end
48
+
49
+ def prometheus_metrics(workers)
50
+ metrics = collect_prometheus_metrics(workers)
51
+ render_metrics_template(metrics)
52
+ end
53
+
54
+ def collect_prometheus_metrics(workers)
55
+ busy = Hash.new(0)
56
+ pending = Hash.new(0)
57
+ workers.each { |w| accumulate_worker_metrics(w, busy, pending) }
58
+ {
59
+ busy:,
60
+ pending:,
61
+ subjects: (busy.keys | pending.keys).sort,
62
+ total: workers.size,
63
+ }
64
+ end
65
+
66
+ def accumulate_worker_metrics(worker, busy, pending)
67
+ service = worker.instance_variable_get(:@service)
68
+ return unless service
69
+
70
+ service.endpoints.each do |ep|
71
+ # TODO: use ep.handler once nats-pure.rb adds attr_reader :handler to NATS::Service::Endpoint
72
+ sub = ep.instance_variable_get(:@handler)
73
+ next unless sub
74
+
75
+ subj = ep.subject.to_s
76
+ busy[subj] += sub.concurrency_semaphore.available_permits.zero? ? 1 : 0
77
+ pending[subj] += sub.pending_queue&.size.to_i
78
+ end
79
+ end
80
+
81
+ def render_metrics_template(metrics)
82
+ ERB.new(File.read(metrics_template_path), trim_mode: '-').result_with_hash(metrics)
83
+ end
84
+
85
+ def metrics_template_path
86
+ File.expand_path('templates/prometheus_metrics.erb', __dir__)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -6,6 +6,7 @@ require 'dry/configurable'
6
6
  require 'concurrent'
7
7
  require_relative '../leopard'
8
8
  require_relative 'message_wrapper'
9
+ require_relative 'metrics_server'
9
10
 
10
11
  module Rubyists
11
12
  module Leopard
@@ -24,6 +25,8 @@ module Rubyists
24
25
  Endpoint = Struct.new(:name, :subject, :queue, :group, :handler)
25
26
 
26
27
  module ClassMethods
28
+ include MetricsServer
29
+
27
30
  def endpoints = @endpoints ||= []
28
31
  def groups = @groups ||= {}
29
32
  def middleware = @middleware ||= []
@@ -78,6 +81,7 @@ module Rubyists
78
81
  pool = spawn_instances(nats_url, service_opts, instances, workers, blocking)
79
82
  logger.info 'Setting up signal trap...'
80
83
  trap_signals(workers, pool)
84
+ start_metrics_server(workers) if ENV['LEOPARD_METRICS_PORT']
81
85
  return pool unless blocking
82
86
 
83
87
  sleep
@@ -0,0 +1,17 @@
1
+ # HELP leopard_subject_busy_instances Instances currently processing a message on this subject
2
+ # TYPE leopard_subject_busy_instances gauge
3
+ <% subjects.each do |subject| -%>
4
+ leopard_subject_busy_instances{subject="<%= subject %>"} <%= busy[subject] %>
5
+ <% end -%>
6
+
7
+ # HELP leopard_subject_total_instances Total Leopard instances in this process
8
+ # TYPE leopard_subject_total_instances gauge
9
+ <% subjects.each do |subject| -%>
10
+ leopard_subject_total_instances{subject="<%= subject %>"} <%= total %>
11
+ <% end -%>
12
+
13
+ # HELP leopard_subject_pending_messages Messages pending processing across all instances
14
+ # TYPE leopard_subject_pending_messages gauge
15
+ <% subjects.each do |subject| -%>
16
+ leopard_subject_pending_messages{subject="<%= subject %>"} <%= pending[subject] %>
17
+ <% end -%>
@@ -3,7 +3,7 @@
3
3
  module Rubyists
4
4
  module Leopard
5
5
  # x-release-please-start-version
6
- VERSION = '0.2.4'
6
+ VERSION = '0.2.5'
7
7
  # x-release-please-end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leopard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - bougyman
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-03-31 00:00:00.000000000 Z
10
+ date: 2026-04-16 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: concurrent-ruby
@@ -103,8 +103,10 @@ files:
103
103
  - lib/leopard.rb
104
104
  - lib/leopard/errors.rb
105
105
  - lib/leopard/message_wrapper.rb
106
+ - lib/leopard/metrics_server.rb
106
107
  - lib/leopard/nats_api_server.rb
107
108
  - lib/leopard/settings.rb
109
+ - lib/leopard/templates/prometheus_metrics.erb
108
110
  - lib/leopard/version.rb
109
111
  homepage: https://github.com/rubyists/leopard
110
112
  licenses: