diff --git a/lib/instana/backend/gc_snapshot.rb b/lib/instana/backend/gc_snapshot.rb index 14e288e1..497b1b9d 100644 --- a/lib/instana/backend/gc_snapshot.rb +++ b/lib/instana/backend/gc_snapshot.rb @@ -17,18 +17,18 @@ def initialize @last_minor_count = 0 end - def report + def report(poll_rate = 1) stats = ::GC.stat total_time = ::GC::Profiler.total_time * 1000 ::GC::Profiler.clear payload = { - totalTime: total_time, + totalTime: total_time / poll_rate, heap_live: stats[:heap_live_slots] || stats[:heap_live_num], heap_free: stats[:heap_free_slots] || stats[:heap_free_num], - minorGcs: stats[:minor_gc_count] - @last_minor_count, - majorGcs: stats[:major_gc_count] - @last_major_count + minorGcs: (stats[:minor_gc_count] - @last_minor_count) / poll_rate.to_f, + majorGcs: (stats[:major_gc_count] - @last_major_count) / poll_rate.to_f } @last_major_count = stats[:major_gc_count] diff --git a/lib/instana/backend/host_agent_reporting_observer.rb b/lib/instana/backend/host_agent_reporting_observer.rb index beb55252..fbab3608 100644 --- a/lib/instana/backend/host_agent_reporting_observer.rb +++ b/lib/instana/backend/host_agent_reporting_observer.rb @@ -161,7 +161,8 @@ def metrics_payload(discovery) end if ::Instana.config[:metrics][:gc][:enabled] - payload[:gc] = GCSnapshot.instance.report + poll_rate = discovery&.dig('plugin', 'ruby', 'poll_rate') || 1 + payload[:gc] = GCSnapshot.instance.report(poll_rate) end if ::Instana.config[:metrics][:thread][:enabled] diff --git a/test/backend/gc_snapshot_test.rb b/test/backend/gc_snapshot_test.rb index b446f7b7..eb14357b 100644 --- a/test/backend/gc_snapshot_test.rb +++ b/test/backend/gc_snapshot_test.rb @@ -4,8 +4,108 @@ require 'test_helper' class GcSnapshotTest < Minitest::Test - def test_report + def test_report_returns_hash subject = Instana::Backend::GCSnapshot.instance - assert subject.report.is_a?(Hash) + assert subject.report(1).is_a?(Hash) + end + + def test_report_contains_expected_keys + subject = Instana::Backend::GCSnapshot.instance + result = subject.report(1) + assert result.key?(:totalTime) + assert result.key?(:heap_live) + assert result.key?(:heap_free) + assert result.key?(:minorGcs) + assert result.key?(:majorGcs) + end + + def test_report_normalizes_gc_counts_by_poll_rate + # Force some GC activity so minor/major counts may differ; we stub GC.stat to control values + subject = Instana::Backend::GCSnapshot.instance + + # Reset baseline counts via a first report + subject.report(1) + + # Stub GC.stat to return predictable delta values relative to the baseline + fake_stats = { + heap_live_slots: 100_000, + heap_free_slots: 20_000, + minor_gc_count: subject.instance_variable_get(:@last_minor_count) + 10, + major_gc_count: subject.instance_variable_get(:@last_major_count) + 2 + } + + ::GC.stub(:stat, fake_stats) do + ::GC::Profiler.stub(:total_time, 0.5) do + result = subject.report(5) + assert_in_delta 2.0, result[:minorGcs], 0.001 # 10 / 5.0 + assert_in_delta 0.4, result[:majorGcs], 0.001 # 2 / 5.0 + end + end + end + + def test_report_normalizes_total_time_by_poll_rate + subject = Instana::Backend::GCSnapshot.instance + subject.report(1) # establish baseline + + fake_stats = { + heap_live_slots: 50_000, + heap_free_slots: 10_000, + minor_gc_count: subject.instance_variable_get(:@last_minor_count), + major_gc_count: subject.instance_variable_get(:@last_major_count) + } + + ::GC.stub(:stat, fake_stats) do + # total_time is in seconds; report multiplies by 1000 then divides by poll_rate + ::GC::Profiler.stub(:total_time, 1.0) do + result_poll1 = subject.report(1) + assert_in_delta 1000.0, result_poll1[:totalTime], 0.001 # 1.0 * 1000 / 1 + end + end + + ::GC.stub(:stat, fake_stats) do + ::GC::Profiler.stub(:total_time, 1.0) do + result_poll5 = subject.report(5) + assert_in_delta 200.0, result_poll5[:totalTime], 0.001 # 1.0 * 1000 / 5 + end + end + end + + def test_report_poll_rate_1_returns_raw_gc_counts + subject = Instana::Backend::GCSnapshot.instance + subject.report(1) # establish baseline + + fake_stats = { + heap_live_slots: 80_000, + heap_free_slots: 15_000, + minor_gc_count: subject.instance_variable_get(:@last_minor_count) + 3, + major_gc_count: subject.instance_variable_get(:@last_major_count) + 1 + } + + ::GC.stub(:stat, fake_stats) do + ::GC::Profiler.stub(:total_time, 0.0) do + result = subject.report(1) + assert_in_delta 3.0, result[:minorGcs], 0.001 # 3 / 1.0 + assert_in_delta 1.0, result[:majorGcs], 0.001 # 1 / 1.0 + end + end + end + + def test_report_heap_slots_are_not_normalized + subject = Instana::Backend::GCSnapshot.instance + + fake_stats = { + heap_live_slots: 42_000, + heap_free_slots: 8_000, + minor_gc_count: subject.instance_variable_get(:@last_minor_count), + major_gc_count: subject.instance_variable_get(:@last_major_count) + } + + ::GC.stub(:stat, fake_stats) do + ::GC::Profiler.stub(:total_time, 0.0) do + result = subject.report(10) + assert_equal 42_000, result[:heap_live] + assert_equal 8_000, result[:heap_free] + end + end end end diff --git a/test/backend/host_agent_reporting_observer_test.rb b/test/backend/host_agent_reporting_observer_test.rb index 0ebcd650..39576bf8 100644 --- a/test/backend/host_agent_reporting_observer_test.rb +++ b/test/backend/host_agent_reporting_observer_test.rb @@ -319,6 +319,82 @@ def test_poll_rate_changes_metrics_timer_interval assert_equal 1, subject.traces_timer.opts[:execution_interval] end + def test_gc_metrics_normalized_by_poll_rate + # Verify that poll_rate from discovery is forwarded to GCSnapshot.report + # and the resulting payload reflects per-second normalization + gc_snapshot = Instana::Backend::GCSnapshot.instance + gc_snapshot.report(1) # establish baseline + + fake_stats = { + heap_live_slots: 60_000, + heap_free_slots: 12_000, + minor_gc_count: gc_snapshot.instance_variable_get(:@last_minor_count) + 10, + major_gc_count: gc_snapshot.instance_variable_get(:@last_major_count) + 4 + } + + reported_poll_rate = nil + ::GC.stub(:stat, fake_stats) do + ::GC::Profiler.stub(:total_time, 0.0) do + stub_request(:post, "http://10.10.10.10:9292/tracermetrics").to_return(status: 200) + stub_request(:post, "http://10.10.10.10:9292/com.instana.plugin.ruby.0") + .with(body: lambda { |raw| + data = JSON.parse(raw) + if data['gc'] + reported_poll_rate = data['gc'] + end + true + }) + .to_return(status: 200) + + client = Instana::Backend::RequestClient.new('10.10.10.10', 9292) + discovery = Concurrent::Atom.new({'pid' => 0, 'plugin' => {'ruby' => {'poll_rate' => 5}}}) + + subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer) + subject.metrics_timer.block.call + end + end + + # Confirm the GC counts in the payload are normalized by poll_rate=5 + assert_in_delta 2.0, reported_poll_rate['minorGcs'], 0.001 # 10 / 5 + assert_in_delta 0.8, reported_poll_rate['majorGcs'], 0.001 # 4 / 5 + end + + def test_gc_metrics_default_poll_rate_when_missing + # When discovery payload has no poll_rate, defaults to 1 (no normalization) + gc_snapshot = Instana::Backend::GCSnapshot.instance + gc_snapshot.report(1) # establish baseline + + fake_stats = { + heap_live_slots: 70_000, + heap_free_slots: 5_000, + minor_gc_count: gc_snapshot.instance_variable_get(:@last_minor_count) + 6, + major_gc_count: gc_snapshot.instance_variable_get(:@last_major_count) + 2 + } + + reported_gc = nil + ::GC.stub(:stat, fake_stats) do + ::GC::Profiler.stub(:total_time, 0.0) do + stub_request(:post, "http://10.10.10.10:9292/tracermetrics").to_return(status: 200) + stub_request(:post, "http://10.10.10.10:9292/com.instana.plugin.ruby.0") + .with(body: lambda { |raw| + data = JSON.parse(raw) + reported_gc = data['gc'] if data['gc'] + true + }) + .to_return(status: 200) + + client = Instana::Backend::RequestClient.new('10.10.10.10', 9292) + # No poll_rate key in discovery + discovery = Concurrent::Atom.new({'pid' => 0}) + + subject = Instana::Backend::HostAgentReportingObserver.new(client, discovery, timer_class: MockTimer) + subject.metrics_timer.block.call + end + end + + # With poll_rate defaulting to 1, counts should be unscaled + assert_in_delta 6.0, reported_gc['minorGcs'], 0.001 + assert_in_delta 2.0, reported_gc['majorGcs'], 0.001 # ============================================================================ # OTLP EXPORT TESTS (driven by ::Instana.config[:otlp]) # ============================================================================