Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions lib/instana/backend/gc_snapshot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
3 changes: 2 additions & 1 deletion lib/instana/backend/host_agent_reporting_observer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
104 changes: 102 additions & 2 deletions test/backend/gc_snapshot_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
76 changes: 76 additions & 0 deletions test/backend/host_agent_reporting_observer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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])
# ============================================================================
Expand Down
Loading