From 920980025fb92147cd98a72e0d557a233963aa58 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Tue, 28 Apr 2026 18:35:21 +0530 Subject: [PATCH 1/5] feat(poll_rate): metrics to be reported to agent at configured interval received in discovery payload Signed-off-by: Arjun Rajappa --- .../backend/host_agent_reporting_observer.rb | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/lib/instana/backend/host_agent_reporting_observer.rb b/lib/instana/backend/host_agent_reporting_observer.rb index fd95988f..621e05b2 100644 --- a/lib/instana/backend/host_agent_reporting_observer.rb +++ b/lib/instana/backend/host_agent_reporting_observer.rb @@ -11,7 +11,7 @@ class HostAgentReportingObserver TRACES_DATA_URL = "/com.instana.plugin.ruby/traces.%i".freeze TRACE_METRICS_URL = "/tracermetrics".freeze - attr_reader :report_timer + attr_reader :metrics_timer, :traces_timer # @param [RequestClient] client used to make requests to the backend # @param [Concurrent::Atom] discovery object used to store discovery response in @@ -19,22 +19,51 @@ def initialize(client, discovery, logger: ::Instana.logger, timer_class: Concurr @client = client @discovery = discovery @logger = logger - @report_timer = timer_class.new(execution_interval: 1, run_now: true) { report_to_backend } + @timer_class = timer_class @nonce = Time.now @processor = processor + + # Initialize timers with default 1 second interval + @metrics_timer = @timer_class.new(execution_interval: 1, run_now: true) { report_metrics_to_backend } + @traces_timer = @timer_class.new(execution_interval: 1, run_now: true) { report_traces_to_backend } end def update(time, _old_version, new_version) return unless time > @nonce @nonce = time - new_version.nil? ? @report_timer.shutdown : @report_timer.execute + + if new_version.nil? + @metrics_timer&.shutdown + @traces_timer&.shutdown + else + # Read poll_rate directly from discovery payload + discovery = @discovery.value + poll_rate = discovery&.dig('pollRate') || 1 + + # Only recreate metrics_timer if poll_rate is different from current interval + if @metrics_timer.nil? || @metrics_timer.opts[:execution_interval] != poll_rate + @metrics_timer&.shutdown + @metrics_timer = @timer_class.new(execution_interval: poll_rate, run_now: true) { report_metrics_to_backend } + end + @metrics_timer.execute + + # Traces timer always uses 1 second interval + @traces_timer&.shutdown + @traces_timer = @timer_class.new(execution_interval: 1, run_now: true) { report_traces_to_backend } + @traces_timer.execute + end end private - def report_to_backend + def report_metrics_to_backend report_metrics if ::Instana.config[:metrics][:enabled] + rescue StandardError => e + @logger.error(%(#{e}\n#{e.backtrace.join("\n")})) + end + + def report_traces_to_backend report_traces if ::Instana.config[:tracing][:enabled] report_trace_stats if ::Instana.config[:tracing][:enabled] rescue StandardError => e From 8d84e7e0797dd31242e9c47dc05bff03b7653cae Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Tue, 28 Apr 2026 18:41:25 +0530 Subject: [PATCH 2/5] test(poll_rate): change in tests to accommodate dual timers tasks one for metrics an other for traces Signed-off-by: Arjun Rajappa --- .../host_agent_reporting_observer_test.rb | 66 ++++++++++++++----- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/test/backend/host_agent_reporting_observer_test.rb b/test/backend/host_agent_reporting_observer_test.rb index c0d50b5d..c77a6eed 100644 --- a/test/backend/host_agent_reporting_observer_test.rb +++ b/test/backend/host_agent_reporting_observer_test.rb @@ -10,16 +10,20 @@ def test_start_stop subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer) - refute subject.report_timer.running + refute subject.metrics_timer.running + refute subject.traces_timer.running subject.update(Time.now, nil, true) - assert subject.report_timer.running + assert subject.metrics_timer.running + assert subject.traces_timer.running subject.update(Time.now, nil, nil) - refute subject.report_timer.running + refute subject.metrics_timer.running + refute subject.traces_timer.running subject.update(Time.now - 500, nil, true) - refute subject.report_timer.running + refute subject.metrics_timer.running + refute subject.traces_timer.running end def test_report @@ -33,7 +37,7 @@ def test_report subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer) - subject.report_timer.block.call + subject.metrics_timer.block.call end def test_report_fail @@ -53,7 +57,7 @@ def test_report_fail subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer) - subject.report_timer.block.call + subject.metrics_timer.block.call assert_nil discovery.value end @@ -80,7 +84,7 @@ def test_agent_action subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer) - subject.report_timer.block.call + subject.metrics_timer.block.call end def test_agent_actions @@ -104,7 +108,7 @@ def test_agent_actions subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer) - subject.report_timer.block.call + subject.metrics_timer.block.call end def test_agent_action_error @@ -119,7 +123,7 @@ def test_agent_action_error subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer) - subject.report_timer.block.call + subject.metrics_timer.block.call end def test_disable_metrics @@ -130,7 +134,7 @@ def test_disable_metrics subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer) - subject.report_timer.block.call + subject.metrics_timer.block.call ensure ::Instana.config[:metrics][:enabled] = true end @@ -153,7 +157,7 @@ def test_disable_metrics_memory subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer) - subject.report_timer.block.call + subject.metrics_timer.block.call ensure ::Instana.config[:metrics][:memory][:enabled] = true end @@ -176,7 +180,7 @@ def test_disable_gc subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer) - subject.report_timer.block.call + subject.metrics_timer.block.call ensure ::Instana.config[:metrics][:gc][:enabled] = true end @@ -199,7 +203,7 @@ def test_disable_thread subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer) - subject.report_timer.block.call + subject.metrics_timer.block.call ensure ::Instana.config[:metrics][:thread][:enabled] = true end @@ -212,7 +216,7 @@ def test_disable_tracing subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer) - subject.report_timer.block.call + subject.traces_timer.block.call ensure ::Instana.config[:tracing][:enabled] = true end @@ -235,7 +239,7 @@ def send subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer, processor: processor) - subject.report_timer.block.call + subject.traces_timer.block.call refute_nil discovery.value end @@ -264,7 +268,7 @@ def send subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer, processor: processor) - subject.report_timer.block.call + subject.traces_timer.block.call assert_nil discovery.value end @@ -283,7 +287,35 @@ def send subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer, processor: processor, logger: Logger.new('/dev/null')) - subject.report_timer.block.call + subject.traces_timer.block.call assert_equal({"pid" => 1234}, discovery.value) end + + def test_poll_rate_changes_metrics_timer_interval + client = Instana::Backend::RequestClient.new('10.10.10.10', 9292) + discovery = Concurrent::Atom.new(nil) + + subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer) + + # Initially, metrics_timer should have 1 second interval (default) + assert_equal 1, subject.metrics_timer.opts[:execution_interval] + refute subject.metrics_timer.running + + # Simulate first discovery with pollRate = 1 (should keep 1 second interval) + discovery.swap { {'pid' => 1234, 'pollRate' => 1} } + subject.update(Time.now, nil, true) + assert subject.metrics_timer.running + assert_equal 1, subject.metrics_timer.opts[:execution_interval] + assert_equal({'pid' => 1234, 'pollRate' => 1}, discovery.value) + + # Simulate discovery cycle changing pollRate to 5 seconds + discovery.swap { {'pid' => 1234, 'pollRate' => 5} } + subject.update(Time.now + 1, nil, true) + assert subject.metrics_timer.running + assert_equal 5, subject.metrics_timer.opts[:execution_interval] + assert_equal({'pid' => 1234, 'pollRate' => 5}, discovery.value) + + # Verify traces_timer always stays at 1 second + assert_equal 1, subject.traces_timer.opts[:execution_interval] + end end From a5819172c9fe1f05f5d86831cc18aad66294be76 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Tue, 28 Apr 2026 20:02:09 +0530 Subject: [PATCH 3/5] ci: pin rabbitmq server version to 4.2 Signed-off-by: Arjun Rajappa --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 40a69c63..27453c73 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -58,7 +58,7 @@ executors: - image: quay.io/minio/minio command: ["server", "/data"] - image: s12v/sns - - image: public.ecr.aws/docker/library/rabbitmq:latest + - image: public.ecr.aws/docker/library/rabbitmq:4.2 - image: public.ecr.aws/sprig/elasticmq-native - image: public.ecr.aws/docker/library/mongo:5-focal mysql2: From 2ad596d49f61709f7dca7bafb8821ad6277f0640 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Fri, 1 May 2026 10:17:45 +0530 Subject: [PATCH 4/5] feat(poll_rate): add thread safety to agent calls Signed-off-by: Arjun Rajappa --- .../backend/host_agent_reporting_observer.rb | 6 ++--- lib/instana/backend/request_client.rb | 27 +++++++++++++++++-- lib/instana/util.rb | 16 +++++------ .../host_agent_reporting_observer_test.rb | 12 ++++----- test/support/mock_timer.rb | 4 +++ 5 files changed, 46 insertions(+), 19 deletions(-) diff --git a/lib/instana/backend/host_agent_reporting_observer.rb b/lib/instana/backend/host_agent_reporting_observer.rb index 621e05b2..cbb914e4 100644 --- a/lib/instana/backend/host_agent_reporting_observer.rb +++ b/lib/instana/backend/host_agent_reporting_observer.rb @@ -37,12 +37,12 @@ def update(time, _old_version, new_version) @metrics_timer&.shutdown @traces_timer&.shutdown else - # Read poll_rate directly from discovery payload + # Read poll_rate from discovery payload - it's nested under plugin.ruby.poll_rate discovery = @discovery.value - poll_rate = discovery&.dig('pollRate') || 1 + poll_rate = discovery&.dig('plugin', 'ruby', 'poll_rate') || 1 # Only recreate metrics_timer if poll_rate is different from current interval - if @metrics_timer.nil? || @metrics_timer.opts[:execution_interval] != poll_rate + if @metrics_timer.nil? || @metrics_timer.execution_interval != poll_rate @metrics_timer&.shutdown @metrics_timer = @timer_class.new(execution_interval: poll_rate, run_now: true) { report_metrics_to_backend } end diff --git a/lib/instana/backend/request_client.rb b/lib/instana/backend/request_client.rb index 8eaa7a64..9f6ab8d1 100644 --- a/lib/instana/backend/request_client.rb +++ b/lib/instana/backend/request_client.rb @@ -38,7 +38,10 @@ def initialize(host, port, use_ssl: false) timeout = Integer(ENV.fetch('INSTANA_TIMEOUT', 500)) @host = host @port = port - @client = Net::HTTP.start(host, port, use_ssl: use_ssl, read_timeout: timeout) + @use_ssl = use_ssl + @timeout = timeout + @client_mutex = Mutex.new + @client = nil end # Send a request to the backend. If data is a {Hash}, @@ -60,7 +63,10 @@ def send_request(method, path, data = nil, headers = {}) data end begin - response = @client.send_request(method, path, body, headers) + response = @client_mutex.synchronize do + ensure_connection + @client.send_request(method, path, body, headers) + end Response.new(response) rescue Errno::ECONNREFUSED => e Instana.logger.debug("Connection refused to #{@host}:#{@port} - #{e.message}") @@ -74,6 +80,11 @@ def send_request(method, path, data = nil, headers = {}) rescue SocketError => e Instana.logger.debug("Socket error connecting to #{@host}:#{@port} - #{e.message}") create_error_response('502', 'Socket Error', 'Socket error', e.message) + rescue IOError => e + Instana.logger.debug("IO error sending request to #{@host}:#{@port} - #{e.message}") + # Reset connection on IO errors and retry once + @client_mutex.synchronize { reset_connection } + create_error_response('500', 'IO Error', 'IOError', e.message) rescue StandardError => e Instana.logger.debug("Error sending request to #{@host}:#{@port} - #{e.class}: #{e.message}") create_error_response('500', 'Internal Error', e.class.to_s, e.message) @@ -82,6 +93,18 @@ def send_request(method, path, data = nil, headers = {}) private + def ensure_connection + return if @client && !@client.instance_variable_get(:@socket).nil? + + reset_connection + @client = Net::HTTP.start(@host, @port, use_ssl: @use_ssl, read_timeout: @timeout) + end + + def reset_connection + @client&.finish rescue nil + @client = nil + end + def encode_body(data) # :nocov: INSTANA_USE_OJ ? Oj.dump(data, mode: :strict) : JSON.dump(data) diff --git a/lib/instana/util.rb b/lib/instana/util.rb index f82e2940..cc016207 100644 --- a/lib/instana/util.rb +++ b/lib/instana/util.rb @@ -27,25 +27,25 @@ def get_rb_source(file) def take_snapshot data = {} - data[:sensorVersion] = ::Instana::VERSION - data[:ruby_version] = RUBY_VERSION + data[:sensorVersion] = ::Instana::VERSION.dup + data[:ruby_version] = RUBY_VERSION.dup data[:rpl] = RUBY_PATCHLEVEL if defined?(RUBY_PATCHLEVEL) # Framework Detection if defined?(::RailsLts::VERSION) - data[:framework] = "Rails on Rails LTS-#{::RailsLts::VERSION}" + data[:framework] = "Rails on Rails LTS-#{::RailsLts::VERSION}".dup elsif defined?(::Rails.version) - data[:framework] = "Ruby on Rails #{::Rails.version}" + data[:framework] = "Ruby on Rails #{::Rails.version}".dup elsif defined?(::Grape::VERSION) - data[:framework] = "Grape #{::Grape::VERSION}" + data[:framework] = "Grape #{::Grape::VERSION}".dup elsif defined?(::Padrino::VERSION) - data[:framework] = "Padrino #{::Padrino::VERSION}" + data[:framework] = "Padrino #{::Padrino::VERSION}".dup elsif defined?(::Sinatra::VERSION) - data[:framework] = "Sinatra #{::Sinatra::VERSION}" + data[:framework] = "Sinatra #{::Sinatra::VERSION}".dup end # Report Bundle @@ -53,7 +53,7 @@ def take_snapshot data[:versions] = {} Gem.loaded_specs.each do |k, v| - data[:versions][k] = v.version.to_s + data[:versions][k.dup] = v.version.to_s.dup end end diff --git a/test/backend/host_agent_reporting_observer_test.rb b/test/backend/host_agent_reporting_observer_test.rb index c77a6eed..c04b2be0 100644 --- a/test/backend/host_agent_reporting_observer_test.rb +++ b/test/backend/host_agent_reporting_observer_test.rb @@ -301,19 +301,19 @@ def test_poll_rate_changes_metrics_timer_interval assert_equal 1, subject.metrics_timer.opts[:execution_interval] refute subject.metrics_timer.running - # Simulate first discovery with pollRate = 1 (should keep 1 second interval) - discovery.swap { {'pid' => 1234, 'pollRate' => 1} } + # Simulate first discovery with poll_rate = 1 (should keep 1 second interval) + discovery.swap { {'pid' => 1234, 'plugin' => {'ruby' => {'poll_rate' => 1}}} } subject.update(Time.now, nil, true) assert subject.metrics_timer.running assert_equal 1, subject.metrics_timer.opts[:execution_interval] - assert_equal({'pid' => 1234, 'pollRate' => 1}, discovery.value) + assert_equal({'pid' => 1234, 'plugin' => {'ruby' => {'poll_rate' => 1}}}, discovery.value) - # Simulate discovery cycle changing pollRate to 5 seconds - discovery.swap { {'pid' => 1234, 'pollRate' => 5} } + # Simulate discovery cycle changing poll_rate to 5 seconds + discovery.swap { {'pid' => 1234, 'plugin' => {'ruby' => {'poll_rate' => 5}}} } subject.update(Time.now + 1, nil, true) assert subject.metrics_timer.running assert_equal 5, subject.metrics_timer.opts[:execution_interval] - assert_equal({'pid' => 1234, 'pollRate' => 5}, discovery.value) + assert_equal({'pid' => 1234, 'plugin' => {'ruby' => {'poll_rate' => 5}}}, discovery.value) # Verify traces_timer always stays at 1 second assert_equal 1, subject.traces_timer.opts[:execution_interval] diff --git a/test/support/mock_timer.rb b/test/support/mock_timer.rb index b0c2c435..2d69dc32 100644 --- a/test/support/mock_timer.rb +++ b/test/support/mock_timer.rb @@ -10,6 +10,10 @@ def initialize(*args, &blk) @running = false end + def execution_interval + @opts[:execution_interval] + end + def shutdown @running = false end From 7e2cc84c264cdbb7e8dd1cda15ff9426a73ef7b0 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Fri, 1 May 2026 10:34:50 +0530 Subject: [PATCH 5/5] test: fix lintin errors Signed-off-by: Arjun Rajappa --- lib/instana/backend/request_client.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/instana/backend/request_client.rb b/lib/instana/backend/request_client.rb index 9f6ab8d1..b971de39 100644 --- a/lib/instana/backend/request_client.rb +++ b/lib/instana/backend/request_client.rb @@ -101,7 +101,11 @@ def ensure_connection end def reset_connection - @client&.finish rescue nil + begin + @client&.finish + rescue + nil + end @client = nil end