Skip to content

Commit

Permalink
Add process name and ID tags to GVL metrics
Browse files Browse the repository at this point in the history
When reporting the GVL metrics, which measure the time spent by the
Ruby VM in its global VM lock, and as such are per-process metrics,
add the process name and process ID as tags, alongside the existing
hostname tag.

As the process name can change during the lifetime of the process,
store it when initialising the GVL probe, to prevent potentially
reporting the same metric with different tag combinations.

To attempt to account for custom process names, keep only the first
word of the process name, after removing path prefixes from it.

Fixes #1106.
  • Loading branch information
unflxw committed Jun 27, 2024
1 parent d299589 commit e9aa060
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 5 deletions.
6 changes: 6 additions & 0 deletions .changesets/report-gvl-metrics-per-process.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
bump: patch
type: change
---

Report Global VM Lock metrics per process. In addition to the existing `hostname` tag, add `process_name` and `process_id` tags to the `gvl_global_timer` and `gvl_waiting_threads` metrics emitted by the [GVL probe](https://docs.appsignal.com/ruby/integrations/global-vm-lock.html), allowing these metrics to be tracked in a per-process basis.
26 changes: 24 additions & 2 deletions lib/appsignal/probes/gvl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ def initialize(appsignal: Appsignal, gvl_tools: ::GVLTools)
Appsignal.internal_logger.debug("Initializing GVL probe")
@appsignal = appsignal
@gvl_tools = gvl_tools

# Store the process name and ID at initialization time
# to avoid picking up changes to the process name at runtime
@process_name = File.basename($PROGRAM_NAME).split.first || "[unknown process]"
@process_id = Process.pid
end

def call
Expand All @@ -39,13 +44,30 @@ def probe_global_timer
gauge_delta :gvl_global_timer, monotonic_time_ns do |time_delta_ns|
if time_delta_ns > 0
time_delta_ms = time_delta_ns / 1_000_000
set_gauge_with_hostname("gvl_global_timer", time_delta_ms)
set_gauges_with_hostname_and_process(
"gvl_global_timer",
time_delta_ms
)
end
end
end

def probe_waiting_threads
set_gauge_with_hostname("gvl_waiting_threads", @gvl_tools::WaitingThreads.count)
set_gauges_with_hostname_and_process(
"gvl_waiting_threads",
@gvl_tools::WaitingThreads.count
)
end

def set_gauges_with_hostname_and_process(name, value)
set_gauge_with_hostname(name, value, {
:process_name => @process_name,
:process_id => @process_id
})

# Also set the gauge without the process name and ID for
# compatibility with existing automated dashboards
set_gauge_with_hostname(name, value)
end
end
end
Expand Down
83 changes: 80 additions & 3 deletions spec/lib/appsignal/probes/gvl_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@

let(:hostname) { "some-host" }

around do |example|
real_program_name = $PROGRAM_NAME
example.run
ensure
$PROGRAM_NAME = real_program_name
end

def gauges_for(metric)
gauges = appsignal_mock.gauges.select do |gauge|
gauge[0] == metric
Expand All @@ -14,7 +21,7 @@ def gauges_for(metric)
end
end

after(:each) { FakeGVLTools.reset }
after { FakeGVLTools.reset }

it "gauges the global timer delta" do
FakeGVLTools::GlobalTimer.monotonic_time = 100_000_000
Expand All @@ -26,6 +33,11 @@ def gauges_for(metric)
probe.call

expect(gauges_for("gvl_global_timer")).to eq [
[200, {
:hostname => hostname,
:process_name => "rspec",
:process_id => Process.pid
}],
[200, { :hostname => hostname }]
]
end
Expand Down Expand Up @@ -58,7 +70,7 @@ def gauges_for(metric)
end

context "when the waiting threads count is enabled" do
before(:each) do
before do
FakeGVLTools::WaitingThreads.enabled = true
end

Expand All @@ -67,13 +79,18 @@ def gauges_for(metric)
probe.call

expect(gauges_for("gvl_waiting_threads")).to eq [
[3, {
:hostname => hostname,
:process_name => "rspec",
:process_id => Process.pid
}],
[3, { :hostname => hostname }]
]
end
end

context "when the waiting threads count is disabled" do
before(:each) do
before do
FakeGVLTools::WaitingThreads.enabled = false
end

Expand All @@ -84,4 +101,64 @@ def gauges_for(metric)
expect(gauges_for("gvl_waiting_threads")).to be_empty
end
end

context "when the process name is a custom value" do
before do
FakeGVLTools::WaitingThreads.enabled = true
end

it "uses only the first word as the process name" do
$PROGRAM_NAME = "sidekiq 7.1.6 app [0 of 5 busy]"
probe.call

expect(gauges_for("gvl_waiting_threads")).to eq [
[0, {
:hostname => hostname,
:process_name => "sidekiq",
:process_id => Process.pid
}],
[0, { :hostname => hostname }]
]
end
end

context "when the process name is a path" do
before do
FakeGVLTools::WaitingThreads.enabled = true
end

it "uses only the binary name as the process name" do
$PROGRAM_NAME = "/foo/folder with spaces/bin/rails"
probe.call

expect(gauges_for("gvl_waiting_threads")).to eq [
[0, {
:hostname => hostname,
:process_name => "rails",
:process_id => Process.pid
}],
[0, { :hostname => hostname }]
]
end
end

context "when the process name is an empty string" do
before do
FakeGVLTools::WaitingThreads.enabled = true
end

it "uses [unknown process] as the process name" do
$PROGRAM_NAME = ""
probe.call

expect(gauges_for("gvl_waiting_threads")).to eq [
[0, {
:hostname => hostname,
:process_name => "[unknown process]",
:process_id => Process.pid
}],
[0, { :hostname => hostname }]
]
end
end
end

0 comments on commit e9aa060

Please sign in to comment.