neetodeploy-autoscale 2.0.0 → 2.0.2

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: 37bcdd5f87684bd29d215c8ed744b8ff6963a6a57d1af07c5ddadc407f4462d5
4
- data.tar.gz: 05fac864a8cca54ac8773e6ec96773d4918961c32fdf1b27ff61049d34241c7f
3
+ metadata.gz: 5f2f285c51fba3a9b5f5b355a7c53dad7e8bf2a6c0bd220c1ba0e3f786319856
4
+ data.tar.gz: 88026ca70f77b913abdb6b13766de130aa18d2da62741b094718a3be191d73c8
5
5
  SHA512:
6
- metadata.gz: cf13f8872227a9e0685460a13cbddeab3d10f5da67c55f96bd533faddb85cb153ded05cebd523d29d83690d09317999dda2fc094aa06b9bcc9e77306bb322603
7
- data.tar.gz: a8809427527fe868c571ee014b79e242379f1ee9ff432e323dfaed5b8ede759f0fd75ca67dccd610bd8c7eadfe358e3415a3f1c4c66ed2d074e1e3073f8de353
6
+ metadata.gz: 8ae6e34da569ea823abfdfc749397a3769562dd106168c5059667811a3f5daa25c8a898ae824afa0c7e976c1977fc4e09d6e15a1a2432ec45a4a7e027c6df1ea
7
+ data.tar.gz: 819da2fae68292133790f2ffa98ec3fb5b5b121064bf113d6cd5e2f63bb79f58dcb7f34ac9592b36bef439bbcc2e394d75933cad3f9d96264707d5b7012a587f
@@ -2,10 +2,12 @@ module Neetodeploy
2
2
  class Config
3
3
  include Singleton
4
4
 
5
- attr_accessor :disable_auto_scale_gem, :app_name, :metrics_server_url, :metrics_server_auth_token, :report_interval_seconds
5
+ attr_accessor :disable_auto_scale_gem, :disable_sidekiq_metrics, :app_name, :metrics_server_url,
6
+ :metrics_server_auth_token, :report_interval_seconds
6
7
 
7
8
  def initialize
8
9
  @disable_auto_scale_gem = ENV["DISABLE_NEETO_DEPLOY_AUTOSCALE"]
10
+ @disable_sidekiq_metrics = ENV["DISABLE_NEETO_DEPLOY_SIDEKIQ_METRICS"]
9
11
  @app_name = ENV["NEETODEPLOY_APP_NAME"]
10
12
  @metrics_server_url = "http://nd-queue-time-exporter-web-deployment:3000/metrics"
11
13
  @metrics_server_auth_token = "K0An3O3MSyEEMTCnRd1IHgGjdGQkzy"
@@ -15,5 +17,9 @@ module Neetodeploy
15
17
  def gem_disabled?
16
18
  disable_auto_scale_gem == "true"
17
19
  end
20
+
21
+ def sidekiq_metrics_disabled?
22
+ disable_sidekiq_metrics == "true"
23
+ end
18
24
  end
19
25
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "neetodeploy/autoscale/config"
4
+
5
+ module Neetodeploy
6
+ module Rails
7
+ class Metrics
8
+ def initialize(env, config = Config.instance)
9
+ @config = config
10
+ @request_start_header = env["HTTP_X_REQUEST_START"].to_i
11
+ @network_time = env["puma.request_body_wait"].to_i
12
+ end
13
+
14
+ def ignore?
15
+ @config.gem_disabled?
16
+ end
17
+
18
+ def queue_time
19
+ return if @request_start_header.zero?
20
+
21
+ time_now = Time.now.to_f * 1000
22
+
23
+ queue_time = (time_now - @request_start_header).round
24
+ queue_time -= @network_time
25
+
26
+ queue_time.positive? ? queue_time : nil
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ require "neetodeploy/autoscale/rails/metrics_store"
5
+ require "neetodeploy/autoscale/reporter"
6
+ require "neetodeploy/autoscale/logger"
7
+
8
+ module Neetodeploy
9
+ module Rails
10
+ class MetricsCollector
11
+ include Singleton
12
+ include NeetoDeploy::Logger
13
+
14
+ def self.start
15
+ instance.start! unless instance.running?
16
+ end
17
+
18
+ def start!
19
+ logger.info("Starting background worker to collect metrics")
20
+ @pid = Process.pid
21
+ start_thread_with_collector_loop
22
+ end
23
+
24
+ def stop!
25
+ @thread&.terminate
26
+ @thread = nil
27
+ @pid = nil
28
+ end
29
+
30
+ def running?
31
+ @pid == Process.pid and @thread.alive?
32
+ end
33
+
34
+ def start_thread_with_collector_loop(config = Config.instance)
35
+ @thread = Thread.new do
36
+ metrics_store = MetricsStore.instance
37
+ loop do
38
+ run_metrics_collection(metrics_store)
39
+ multiplier = 1 - (rand / 4)
40
+ sleep config.report_interval_seconds * multiplier
41
+ end
42
+ end
43
+ end
44
+
45
+ def run_metrics_collection(metrics_store)
46
+ data = metrics_store.flush
47
+ return if data.empty?
48
+
49
+ average_queue_time = data.sum / data.size
50
+ Reporter.new(average_queue_time, "queue_time", "web").report
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+
5
+ module Neetodeploy
6
+ module Rails
7
+ class MetricsStore
8
+ include Singleton
9
+
10
+ attr_reader :metrics
11
+
12
+ def initialize
13
+ @metrics = []
14
+ end
15
+
16
+ def push(queue_time)
17
+ @metrics << queue_time
18
+ end
19
+
20
+ def flush
21
+ result = @metrics
22
+ @metrics = []
23
+ result
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "time"
5
+ require "neetodeploy/autoscale/rails/metrics"
6
+ require "neetodeploy/autoscale/rails/metrics_store"
7
+ require "neetodeploy/autoscale/rails/metrics_collector"
8
+
9
+ module Neetodeploy
10
+ module Rails
11
+ class Middleware
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ def call(env)
17
+ metrics = Metrics.new(env)
18
+ queue_time = metrics.queue_time unless metrics.ignore?
19
+ MetricsCollector.start
20
+
21
+ if queue_time
22
+ store = MetricsStore.instance
23
+ store.push queue_time
24
+ end
25
+
26
+ @app.call(env)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "neetodeploy/autoscale/rails/middleware"
4
+ module Neetodeploy
5
+ module Rails
6
+ class Railtie < ::Rails::Railtie
7
+ initializer "neetodeploy.Neetodeploy.middleware" do |app|
8
+ app.middleware.insert_before Rack::Runtime, Neetodeploy::Rails::Middleware
9
+ end
10
+
11
+ config.after_initialize do
12
+ Neetodeploy::Rails::MetricsCollector.start
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "neetodeploy/autoscale/rails/metrics"
4
+ require "neetodeploy/autoscale/rails/metrics_store"
5
+ require "neetodeploy/autoscale/rails/metrics_collector"
6
+ require "neetodeploy/autoscale/rails/middleware"
7
+ require "neetodeploy/autoscale/rails/railtie"
@@ -3,19 +3,41 @@
3
3
  require "net/http"
4
4
  require "time"
5
5
  require "neetodeploy/autoscale/config"
6
+ require "neetodeploy/autoscale/logger"
6
7
 
7
8
  module Neetodeploy
8
9
  class Reporter
10
+ include NeetoDeploy::Logger
9
11
 
10
- def initialize(queue_time, config = Config.instance )
12
+ def initialize(queue_time, metric_name, process_type, queue_name = nil, config = Config.instance)
11
13
  @queue_time = queue_time.to_i
12
14
  @config = config
15
+ @metric_name = metric_name
16
+ @queue_name = queue_name
17
+ @process_type = process_type
13
18
  end
14
19
 
15
20
  def report
16
- url = URI.parse(@config.metrics_server_url)
17
- url.query = URI.encode_www_form(app_name: @config.app_name, process_type: "web", queue_time: @queue_time)
21
+ params = common_params
22
+ params[:queue_name] = @queue_name if @queue_name
23
+ url = build_url(params)
24
+ logger.info("Reporting average queue time for #{@process_type} dyno: #{@queue_time}")
18
25
 
26
+ post_request(url)
27
+ end
28
+
29
+ private
30
+
31
+ def common_params
32
+ {
33
+ app_name: @config.app_name,
34
+ queue_time: @queue_time,
35
+ metric_name: @metric_name,
36
+ process_type: @process_type
37
+ }
38
+ end
39
+
40
+ def post_request(url)
19
41
  post = Net::HTTP::Post.new(url)
20
42
  post["AuthToken"] = @config.metrics_server_auth_token
21
43
 
@@ -23,5 +45,12 @@ module Neetodeploy
23
45
  http.request(post)
24
46
  end
25
47
  end
48
+
49
+ def build_url(params = {})
50
+ exporter_url = URI.parse(@config.metrics_server_url)
51
+ exporter_url.query = URI.encode_www_form(params)
52
+
53
+ exporter_url
54
+ end
26
55
  end
27
56
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ require "neetodeploy/autoscale/reporter"
5
+ require "neetodeploy/autoscale/logger"
6
+ require "neetodeploy/autoscale/config"
7
+ require "sidekiq/api"
8
+
9
+ module Neetodeploy
10
+ module Sidekiq
11
+ class MetricsCollector
12
+ include Singleton
13
+ include NeetoDeploy::Logger
14
+
15
+ def self.start
16
+ instance.start! unless instance.running?
17
+ end
18
+
19
+ def start!
20
+ logger.info("Starting Sidekiq metrics collector")
21
+ @pid = Process.pid
22
+ start_thread_with_collector_loop
23
+ end
24
+
25
+ def stop!
26
+ @pid = nil # Set pid to nil first to prevent restart
27
+ @thread&.terminate
28
+ @thread = nil
29
+ end
30
+
31
+ def running?
32
+ @pid == Process.pid and @thread&.alive?
33
+ end
34
+
35
+ def start_thread_with_collector_loop(config = Config.instance)
36
+ @thread = Thread.new do
37
+ loop do
38
+ run_sidekiq_metrics_collection
39
+ multiplier = 1 - (rand / 4)
40
+ sleep config.report_interval_seconds * multiplier
41
+ end
42
+ rescue StandardError => e
43
+ logger.error("Sidekiq metrics collector thread terminated with error: #{e.message}")
44
+ logger.error(e.backtrace.join("\n")) if e.backtrace
45
+ ensure
46
+ if @pid == Process.pid && !@thread.nil?
47
+ logger.info("Restarting Sidekiq metrics collector thread")
48
+ sleep(5)
49
+ start_thread_with_collector_loop
50
+ end
51
+ end
52
+ end
53
+
54
+ def run_sidekiq_metrics_collection
55
+ return unless sidekiq_available?
56
+ return if Config.instance.sidekiq_metrics_disabled?
57
+
58
+ begin
59
+ queues = ::Sidekiq::Queue.all
60
+ return if queues.empty?
61
+
62
+ queues.each do |queue|
63
+ collect_queue_metrics(queue)
64
+ end
65
+ rescue StandardError => e
66
+ logger.error("Error collecting Sidekiq metrics: #{e.message}")
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def sidekiq_available?
73
+ defined?(::Sidekiq) && ::Sidekiq.respond_to?(:redis)
74
+ end
75
+
76
+ def collect_queue_metrics(queue)
77
+ queue_name = queue.name
78
+ latency_ms = (queue.latency * 1000).ceil
79
+
80
+ report_sidekiq_metric(queue_name, latency_ms)
81
+ end
82
+
83
+ def report_sidekiq_metric(queue_name, queue_time)
84
+ return if queue_time.zero?
85
+
86
+ Reporter.new(queue_time, "sidekiq_queue_time", "worker", queue_name).report
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "neetodeploy/autoscale/sidekiq/metrics_collector"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Neetodeploy
4
4
  module Autoscale
5
- VERSION = "2.0.0"
5
+ VERSION = "2.0.2"
6
6
  end
7
7
  end
@@ -9,29 +9,66 @@ module Neetodeploy
9
9
 
10
10
  class << self
11
11
  include NeetoDeploy::Logger
12
-
13
- def setup_middleware
12
+
13
+ def setup_rails_middleware
14
14
  Neetodeploy::Autoscale.setup_rails if defined? Rails
15
15
  end
16
16
 
17
17
  def setup_rails
18
- require "neetodeploy/autoscale/railtie"
18
+ require "neetodeploy/autoscale/rails"
19
+ end
20
+
21
+ def setup_sidekiq_metrics
22
+ require "neetodeploy/autoscale/sidekiq"
23
+
24
+ ::Sidekiq.configure_server do
25
+ Neetodeploy::Sidekiq::MetricsCollector.start
26
+ end
19
27
  end
20
28
 
21
29
  def enable_middleware?
22
- ENV["NEETODEPLOY_APP_NAME"].present? && ENV["DISABLE_NEETO_DEPLOY_AUTOSCALE"] != "true"
30
+ ENV["DISABLE_NEETO_DEPLOY_AUTOSCALE"] != "true" && in_rails_server?
23
31
  end
24
32
 
25
- def initialize_if_enabled
33
+ def enable_sidekiq_metrics?
34
+ ENV["DISABLE_NEETO_DEPLOY_SIDEKIQ_METRICS"] != "true" && defined?(::Sidekiq) && Sidekiq.server?
35
+ end
36
+
37
+ def initialize_rails_middleware
26
38
  if enable_middleware?
27
- logger.info("Setting up Neetodeploy autoscale middleware")
28
- setup_middleware
39
+ logger.info("Setting up Neetodeploy rails autoscale middleware")
40
+ setup_rails_middleware
29
41
  else
30
- logger.info("Conditions not met skipping NeetoDeploy autoscale middleware setup")
42
+ logger.info("Skipping NeetoDeploy rails autoscale middleware")
31
43
  end
32
44
  end
45
+
46
+ def initialize_sidekiq_metrics
47
+ if enable_sidekiq_metrics?
48
+ logger.info("Setting up Neetodeploy Sidekiq metrics middleware")
49
+ setup_sidekiq_metrics
50
+ else
51
+ logger.info("Skipping NeetoDeploy Sidekiq metrics middleware")
52
+ end
53
+ end
54
+
55
+ def initialize_middlewares
56
+ return unless ENV["NEETODEPLOY_APP_NAME"].present?
57
+
58
+ initialize_rails_middleware
59
+ initialize_sidekiq_metrics
60
+ end
61
+
62
+ def in_rails_server?
63
+ # FIX ME: This is a gross way to detect if we're in a Rails server.
64
+ caller_keywords = %w[rails puma unicorn]
65
+ in_console = caller.any? { |call| call.include?("console_command.rb") || call.include?("runner_command.rb") }
66
+ is_rails = caller_keywords.any? { |key| File.basename($0).include?(key) }
67
+ !in_console && is_rails
68
+ end
69
+
33
70
  end
34
71
  end
35
72
  end
36
73
 
37
- Neetodeploy::Autoscale.initialize_if_enabled
74
+ Neetodeploy::Autoscale.initialize_middlewares
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: neetodeploy-autoscale
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sreeram Venkitesh
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-03-19 00:00:00.000000000 Z
11
+ date: 2025-09-23 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: For automatically scaling your Rails application based on network metrics
14
14
  email:
@@ -25,12 +25,15 @@ files:
25
25
  - lib/neetodeploy-autoscale.rb
26
26
  - lib/neetodeploy/autoscale/config.rb
27
27
  - lib/neetodeploy/autoscale/logger.rb
28
- - lib/neetodeploy/autoscale/metrics.rb
29
- - lib/neetodeploy/autoscale/metrics_collector.rb
30
- - lib/neetodeploy/autoscale/metrics_store.rb
31
- - lib/neetodeploy/autoscale/middleware.rb
32
- - lib/neetodeploy/autoscale/railtie.rb
28
+ - lib/neetodeploy/autoscale/rails.rb
29
+ - lib/neetodeploy/autoscale/rails/metrics.rb
30
+ - lib/neetodeploy/autoscale/rails/metrics_collector.rb
31
+ - lib/neetodeploy/autoscale/rails/metrics_store.rb
32
+ - lib/neetodeploy/autoscale/rails/middleware.rb
33
+ - lib/neetodeploy/autoscale/rails/railtie.rb
33
34
  - lib/neetodeploy/autoscale/reporter.rb
35
+ - lib/neetodeploy/autoscale/sidekiq.rb
36
+ - lib/neetodeploy/autoscale/sidekiq/metrics_collector.rb
34
37
  - lib/neetodeploy/autoscale/version.rb
35
38
  homepage: https://neetodeploy.com
36
39
  licenses: []
@@ -53,7 +56,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
53
56
  - !ruby/object:Gem::Version
54
57
  version: '0'
55
58
  requirements: []
56
- rubygems_version: 3.4.19
59
+ rubygems_version: 3.4.1
57
60
  signing_key:
58
61
  specification_version: 4
59
62
  summary: neetoDeploy autoscaler gem
@@ -1,26 +0,0 @@
1
- require "neetodeploy/autoscale/config"
2
-
3
- module Neetodeploy
4
- class Metrics
5
- def initialize(env, config = Config.instance)
6
- @config = config
7
- @request_start_header = env["HTTP_X_REQUEST_START"].to_i
8
- @network_time = env["puma.request_body_wait"].to_i
9
- end
10
-
11
- def ignore?
12
- @config.gem_disabled?
13
- end
14
-
15
- def queue_time
16
- return if @request_start_header.zero?
17
-
18
- time_now = Time.now.to_f * 1000
19
-
20
- queue_time = (time_now - @request_start_header).round
21
- queue_time -= @network_time
22
-
23
- queue_time.positive? ? queue_time : nil
24
- end
25
- end
26
- end
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "singleton"
4
- require "neetodeploy/autoscale/metrics_store"
5
- require "neetodeploy/autoscale/reporter"
6
- require "neetodeploy/autoscale/logger"
7
-
8
- module Neetodeploy
9
- class MetricsCollector
10
- include Singleton
11
- include NeetoDeploy::Logger
12
-
13
- def self.start
14
- instance.start! unless instance.running?
15
- end
16
-
17
- def start!
18
- logger.info("Starting background worker to collect metrics")
19
- @pid = Process.pid
20
- start_thread_with_collector_loop
21
- end
22
-
23
- def stop!
24
- @thread&.terminate
25
- @thread = nil
26
- @pid = nil
27
- end
28
-
29
- def running?
30
- @pid == Process.pid and @thread.alive?
31
- end
32
-
33
- def start_thread_with_collector_loop(config = Config.instance)
34
- @thread = Thread.new do
35
- metrics_store = MetricsStore.instance
36
- loop do
37
- run_metrics_collection(metrics_store)
38
- multiplier = 1 - (rand / 4)
39
- sleep config.report_interval_seconds * multiplier
40
- end
41
- end
42
- end
43
-
44
- def run_metrics_collection(metrics_store)
45
- data = metrics_store.flush
46
- return if data.empty?
47
-
48
- average_queue_time = data.sum / data.size
49
- logger.info("Reporting average queue time of #{data.size} metrics: #{average_queue_time}")
50
- Reporter.new(average_queue_time).report
51
- end
52
- end
53
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "singleton"
4
- require "judoscale/metric"
5
- require "judoscale/report"
6
-
7
- module Neetodeploy
8
- class MetricsStore
9
- include Singleton
10
-
11
- attr_reader :metrics
12
-
13
- def initialize
14
- @metrics = []
15
- end
16
-
17
- def push(queue_time)
18
- @metrics << queue_time
19
- end
20
-
21
- def flush
22
- result = @metrics
23
- @metrics = []
24
- result
25
- end
26
- end
27
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "net/http"
4
- require "time"
5
- require "neetodeploy/autoscale/metrics"
6
- require "neetodeploy/autoscale/metrics_store"
7
- require "neetodeploy/autoscale/metrics_collector"
8
-
9
- module Neetodeploy
10
- class Middleware
11
- def initialize(app)
12
- @app = app
13
- end
14
-
15
- def call(env)
16
- metrics = Metrics.new(env)
17
- queue_time = metrics.queue_time unless metrics.ignore?
18
- MetricsCollector.start
19
-
20
- if queue_time
21
- store = MetricsStore.instance
22
- store.push queue_time
23
- end
24
-
25
- @app.call(env)
26
- end
27
- end
28
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "neetodeploy/autoscale/middleware"
4
- module Neetodeploy
5
- class Railtie < Rails::Railtie
6
- initializer "neetodeploy.Neetodeploy.middleware" do |app|
7
- app.middleware.insert_before Rack::Runtime, Neetodeploy::Middleware
8
- end
9
-
10
- config.after_initialize do
11
- MetricsCollector.start unless in_rails_console_or_runner?
12
- end
13
-
14
- def in_rails_console_or_runner?
15
- # This is gross, but we can't find a more reliable way to detect if we're in a Rails console/runner.
16
- caller.any? { |call| call.include?("console_command.rb") || call.include?("runner_command.rb") }
17
- end
18
- end
19
- end