Module: DeadBro::Collectors::System

Defined in:
lib/dead_bro/collectors/system.rb

Overview

System collector provides best-effort CPU and memory statistics using cgroups when available and falling back to /proc on Linux.

CPU percentages are normalised to 0..100 across all cores. The first run may not contain a CPU percentage because there is no previous sample to diff against.

Constant Summary collapse

CPU_SAMPLE_KEY =
"cpu"
MEMINFO_PATH =
"/proc/meminfo"

Class Method Summary collapse

Class Method Details

.collectObject



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/dead_bro/collectors/system.rb', line 20

def collect
  return {enabled: false} unless system_enabled?

  {
    cpu_pct: cpu_percentage,
    mem_used_bytes: mem_used_bytes,
    mem_total_bytes: mem_total_bytes,
    mem_available_bytes: mem_available_bytes,
    disk: Filesystem.collect
  }
rescue => e
  {
    error_class: e.class.name,
    error_message: e.message.to_s[0, 500]
  }
end

.cpu_pct_from_samples(prev, current) ⇒ Object

Computes a CPU percentage from two /proc/stat samples. This is intentionally public so it can be unit tested.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/dead_bro/collectors/system.rb', line 118

def cpu_pct_from_samples(prev, current)
  prev_total = prev["total"].to_f
  prev_idle = prev["idle"].to_f
  cur_total = current["total"].to_f
  cur_idle = current["idle"].to_f

  total_delta = cur_total - prev_total
  idle_delta = cur_idle - prev_idle
  return nil if total_delta <= 0

  usage = (total_delta - idle_delta) / total_delta.to_f
  pct = (usage * 100.0)
  return nil unless pct.finite?

  pct.round(2)
rescue
  nil
end

.cpu_percentageObject

CPU percentage normalised to 0..100



59
60
61
62
63
64
65
# File 'lib/dead_bro/collectors/system.rb', line 59

def cpu_percentage
  if linux?
    cpu_percentage_linux
  elsif macos?
    cpu_percentage_macos
  end
end

.cpu_percentage_linuxObject



67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/dead_bro/collectors/system.rb', line 67

def cpu_percentage_linux
  return nil unless File.readable?("/proc/stat")

  now = current_time
  current = read_proc_stat
  prev = SampleStore.load(CPU_SAMPLE_KEY)
  SampleStore.save(CPU_SAMPLE_KEY, {"timestamp" => now, "stat" => current})

  return nil unless prev && prev["stat"].is_a?(Hash) && prev["timestamp"]

  cpu_pct_from_samples(prev["stat"], current)
rescue
  nil
end

.cpu_percentage_macosObject



82
83
84
85
86
87
88
89
90
91
# File 'lib/dead_bro/collectors/system.rb', line 82

def cpu_percentage_macos
  output = `top -l 1 -n 0 | grep "CPU usage"`
  # Example: CPU usage: 9.38% user, 10.93% sys, 79.68% idle
  if output =~ /([\d.]+)% idle/
    idle = $1.to_f
    (100.0 - idle).round(2)
  end
rescue
  nil
end

.current_timeObject



93
94
95
96
97
# File 'lib/dead_bro/collectors/system.rb', line 93

def current_time
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
rescue
  Time.now.to_f
end

.linux?Boolean

Returns:

  • (Boolean)


44
45
46
47
48
49
# File 'lib/dead_bro/collectors/system.rb', line 44

def linux?
  host_os = RbConfig::CONFIG["host_os"].to_s.downcase
  host_os.include?("linux")
rescue
  false
end

.macos?Boolean

Returns:

  • (Boolean)


51
52
53
54
55
56
# File 'lib/dead_bro/collectors/system.rb', line 51

def macos?
  host_os = RbConfig::CONFIG["host_os"].to_s.downcase
  host_os.include?("darwin")
rescue
  false
end

.mem_available_bytesObject



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/dead_bro/collectors/system.rb', line 170

def mem_available_bytes
  if linux?
    info = meminfo
    avail_kb = info["MemAvailable"] || info["MemFree"]
    return nil unless avail_kb
    avail_kb * 1024
  elsif macos?
    # vm_stat output:
    # Pages free:                               3632.
    # Pages active:                           138466.
    # Pages inactive:                         134812.
    # ...
    output = `vm_stat`
    pages_free = output[/Pages free:\s+(\d+)/, 1].to_i
    pages_inactive = output[/Pages inactive:\s+(\d+)/, 1].to_i

    # MacOS page size is typically 4096 bytes
    (pages_free + pages_inactive) * 4096
  else
    nil
  end
rescue
  nil
end

.mem_total_bytesObject



157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/dead_bro/collectors/system.rb', line 157

def mem_total_bytes
  if linux?
    info = meminfo
    total_kb = info["MemTotal"]
    return nil unless total_kb
    total_kb * 1024
  elsif macos?
    `sysctl -n hw.memsize`.to_i
  end
rescue
  nil
end

.mem_used_bytesObject



195
196
197
198
199
200
201
202
203
# File 'lib/dead_bro/collectors/system.rb', line 195

def mem_used_bytes
  total = mem_total_bytes
  avail = mem_available_bytes
  return nil unless total && avail

  total - avail
rescue
  nil
end

.meminfoObject



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/dead_bro/collectors/system.rb', line 137

def meminfo
  return {} unless linux? && File.readable?(MEMINFO_PATH)

  info = {}
  File.foreach(MEMINFO_PATH) do |line|
    key, value, _ = line.split
    next unless key && value

    key = key.sub(":", "")
    info[key] = begin
      Integer(value)
    rescue
      nil
    end
  end
  info
rescue
  {}
end

.read_proc_statObject

Parse the first “cpu” line from /proc/stat



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/dead_bro/collectors/system.rb', line 100

def read_proc_stat
  File.foreach("/proc/stat") do |line|
    next unless line.start_with?("cpu ")

    fields = line.split
    # cpu  user nice system idle iowait irq softirq steal guest guest_nice
    values = fields[1..-1].map { |v| v.to_i }
    total = values.sum
    idle = values[3] + values[4] # idle + iowait
    return {"total" => total, "idle" => idle}
  end
  {}
rescue
  {}
end

.system_enabled?Boolean

Returns:

  • (Boolean)


37
38
39
40
41
42
# File 'lib/dead_bro/collectors/system.rb', line 37

def system_enabled?
  DeadBro.configuration.respond_to?(:enable_system_stats) &&
    DeadBro.configuration.enable_system_stats
rescue
  false
end