dbwatcher 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +24 -2
- data/app/assets/config/dbwatcher_manifest.js +1 -0
- data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +196 -119
- data/app/assets/javascripts/dbwatcher/components/dashboard.js +325 -0
- data/app/assets/javascripts/dbwatcher/components/timeline.js +211 -0
- data/app/assets/javascripts/dbwatcher/dbwatcher.js +5 -0
- data/app/assets/stylesheets/dbwatcher/application.css +691 -41
- data/app/assets/stylesheets/dbwatcher/application.scss +5 -0
- data/app/assets/stylesheets/dbwatcher/components/_badges.scss +68 -23
- data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +83 -26
- data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +3 -3
- data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +9 -0
- data/app/assets/stylesheets/dbwatcher/components/_tabulator.scss +248 -0
- data/app/assets/stylesheets/dbwatcher/components/_timeline.scss +326 -0
- data/app/assets/stylesheets/dbwatcher/vendor/_tabulator_overrides.scss +37 -0
- data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +18 -4
- data/app/controllers/dbwatcher/api/v1/system_info_controller.rb +180 -0
- data/app/controllers/dbwatcher/dashboard/system_info_controller.rb +64 -0
- data/app/controllers/dbwatcher/dashboard_controller.rb +17 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +3 -19
- data/app/helpers/dbwatcher/application_helper.rb +43 -11
- data/app/helpers/dbwatcher/diagram_helper.rb +0 -88
- data/app/views/dbwatcher/dashboard/_layout.html.erb +27 -0
- data/app/views/dbwatcher/dashboard/_overview.html.erb +188 -0
- data/app/views/dbwatcher/dashboard/_system_info.html.erb +22 -0
- data/app/views/dbwatcher/dashboard/_system_info_content.html.erb +389 -0
- data/app/views/dbwatcher/dashboard/index.html.erb +8 -177
- data/app/views/dbwatcher/sessions/_layout.html.erb +26 -0
- data/app/views/dbwatcher/sessions/{_summary_tab.html.erb → _summary.html.erb} +1 -1
- data/app/views/dbwatcher/sessions/_tables.html.erb +170 -0
- data/app/views/dbwatcher/sessions/_timeline.html.erb +260 -0
- data/app/views/dbwatcher/sessions/index.html.erb +107 -87
- data/app/views/dbwatcher/sessions/show.html.erb +12 -4
- data/app/views/dbwatcher/tables/index.html.erb +32 -40
- data/app/views/layouts/dbwatcher/application.html.erb +101 -48
- data/config/routes.rb +25 -7
- data/lib/dbwatcher/configuration.rb +18 -1
- data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +102 -1
- data/lib/dbwatcher/services/api/{changes_data_service.rb → tables_data_service.rb} +6 -6
- data/lib/dbwatcher/services/base_service.rb +2 -0
- data/lib/dbwatcher/services/system_info/database_info_collector.rb +263 -0
- data/lib/dbwatcher/services/system_info/machine_info_collector.rb +387 -0
- data/lib/dbwatcher/services/system_info/runtime_info_collector.rb +328 -0
- data/lib/dbwatcher/services/system_info/system_info_collector.rb +114 -0
- data/lib/dbwatcher/services/timeline_data_service/enhancement_utilities.rb +100 -0
- data/lib/dbwatcher/services/timeline_data_service/entry_builder.rb +125 -0
- data/lib/dbwatcher/services/timeline_data_service/metadata_builder.rb +93 -0
- data/lib/dbwatcher/services/timeline_data_service.rb +130 -0
- data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +1 -1
- data/lib/dbwatcher/storage/concerns/error_handler.rb +6 -6
- data/lib/dbwatcher/storage/session.rb +5 -0
- data/lib/dbwatcher/storage/system_info_storage.rb +242 -0
- data/lib/dbwatcher/storage.rb +12 -0
- data/lib/dbwatcher/version.rb +1 -1
- data/lib/dbwatcher.rb +16 -2
- metadata +28 -16
- data/app/helpers/dbwatcher/component_helper.rb +0 -29
- data/app/views/dbwatcher/sessions/_changes_tab.html.erb +0 -265
- data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +0 -12
- data/app/views/dbwatcher/sessions/changes.html.erb +0 -21
- data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +0 -44
- data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +0 -96
- data/app/views/dbwatcher/sessions/diagrams.html.erb +0 -21
- data/app/views/dbwatcher/sessions/shared/_layout.html.erb +0 -8
- data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +0 -35
- data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +0 -25
- data/app/views/dbwatcher/sessions/summary.html.erb +0 -21
- /data/app/views/dbwatcher/sessions/{_diagrams_tab.html.erb → _diagrams.html.erb} +0 -0
@@ -0,0 +1,263 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../logging"
|
4
|
+
|
5
|
+
module Dbwatcher
|
6
|
+
module Services
|
7
|
+
module SystemInfo
|
8
|
+
# Database information collector service
|
9
|
+
#
|
10
|
+
# Collects database-specific information including adapter, version,
|
11
|
+
# connection pool stats, table counts, and schema information.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# info = SystemInfo::DatabaseInfoCollector.call
|
15
|
+
# puts info[:adapter]
|
16
|
+
# puts info[:version]
|
17
|
+
# puts info[:tables].count
|
18
|
+
#
|
19
|
+
# This class is necessarily complex due to the comprehensive database information
|
20
|
+
# it needs to collect across different database systems.
|
21
|
+
# rubocop:disable Metrics/ClassLength
|
22
|
+
class DatabaseInfoCollector
|
23
|
+
include Dbwatcher::Logging
|
24
|
+
|
25
|
+
# Class method to create instance and call
|
26
|
+
#
|
27
|
+
# @return [Hash] database information
|
28
|
+
def self.call
|
29
|
+
new.call
|
30
|
+
end
|
31
|
+
|
32
|
+
def call
|
33
|
+
log_info "#{self.class.name}: Collecting database information"
|
34
|
+
|
35
|
+
{
|
36
|
+
adapter: collect_adapter_info,
|
37
|
+
version: collect_database_version,
|
38
|
+
connection_pool: collect_connection_pool_info,
|
39
|
+
tables: collect_table_info,
|
40
|
+
schema: collect_schema_info,
|
41
|
+
indexes: collect_index_info,
|
42
|
+
query_stats: collect_query_stats
|
43
|
+
}
|
44
|
+
rescue StandardError => e
|
45
|
+
log_error "Database info collection failed: #{e.message}"
|
46
|
+
{ error: e.message }
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Collect database adapter information
|
52
|
+
#
|
53
|
+
# @return [Hash] adapter information
|
54
|
+
def collect_adapter_info
|
55
|
+
return {} unless active_record_available?
|
56
|
+
|
57
|
+
connection = ActiveRecord::Base.connection
|
58
|
+
adapter_name = connection.adapter_name.downcase
|
59
|
+
|
60
|
+
{
|
61
|
+
name: adapter_name,
|
62
|
+
pool_size: ActiveRecord::Base.connection_pool.size,
|
63
|
+
checkout_timeout: ActiveRecord::Base.connection_pool.checkout_timeout
|
64
|
+
}
|
65
|
+
rescue StandardError => e
|
66
|
+
log_error "Failed to get adapter info: #{e.message}"
|
67
|
+
{}
|
68
|
+
end
|
69
|
+
|
70
|
+
# Collect database version information
|
71
|
+
#
|
72
|
+
# @return [String] database version
|
73
|
+
# rubocop:disable Metrics/MethodLength
|
74
|
+
def collect_database_version
|
75
|
+
return nil unless active_record_available?
|
76
|
+
|
77
|
+
connection = ActiveRecord::Base.connection
|
78
|
+
adapter_name = connection.adapter_name.downcase
|
79
|
+
|
80
|
+
case adapter_name
|
81
|
+
when /mysql/
|
82
|
+
connection.select_value("SELECT VERSION()")
|
83
|
+
when /postgresql/
|
84
|
+
connection.select_value("SELECT version()")
|
85
|
+
when /sqlite/
|
86
|
+
connection.select_value("SELECT sqlite_version()")
|
87
|
+
else
|
88
|
+
"unknown"
|
89
|
+
end
|
90
|
+
rescue StandardError => e
|
91
|
+
log_error "Failed to get database version: #{e.message}"
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
# rubocop:enable Metrics/MethodLength
|
95
|
+
|
96
|
+
# Collect connection pool information
|
97
|
+
#
|
98
|
+
# @return [Hash] connection pool statistics
|
99
|
+
def collect_connection_pool_info
|
100
|
+
return {} unless active_record_available?
|
101
|
+
|
102
|
+
pool = ActiveRecord::Base.connection_pool
|
103
|
+
{
|
104
|
+
size: pool.size,
|
105
|
+
connections: pool.connections.size,
|
106
|
+
active: pool.active_connection?,
|
107
|
+
checkout_timeout: pool.checkout_timeout,
|
108
|
+
reaper_frequency: pool.reaper&.frequency
|
109
|
+
}
|
110
|
+
rescue StandardError => e
|
111
|
+
log_error "Failed to get connection pool info: #{e.message}"
|
112
|
+
{}
|
113
|
+
end
|
114
|
+
|
115
|
+
# Collect table information
|
116
|
+
#
|
117
|
+
# @return [Hash] table statistics
|
118
|
+
# rubocop:disable Metrics/MethodLength
|
119
|
+
def collect_table_info
|
120
|
+
return {} unless active_record_available?
|
121
|
+
|
122
|
+
connection = ActiveRecord::Base.connection
|
123
|
+
tables = connection.tables
|
124
|
+
|
125
|
+
# Skip detailed table info if performance metrics are disabled
|
126
|
+
return { count: tables.size } unless Dbwatcher.configuration.system_info_include_performance_metrics
|
127
|
+
|
128
|
+
table_info = { count: tables.size, tables: [] }
|
129
|
+
|
130
|
+
tables.each do |table|
|
131
|
+
count = connection.select_value("SELECT COUNT(*) FROM #{connection.quote_table_name(table)}").to_i
|
132
|
+
table_info[:tables] << {
|
133
|
+
name: table,
|
134
|
+
count: count,
|
135
|
+
has_primary_key: connection.primary_key(table).present?
|
136
|
+
}
|
137
|
+
rescue StandardError => e
|
138
|
+
log_error "Failed to get info for table #{table}: #{e.message}"
|
139
|
+
table_info[:tables] << { name: table, error: e.message }
|
140
|
+
end
|
141
|
+
|
142
|
+
table_info
|
143
|
+
rescue StandardError => e
|
144
|
+
log_error "Failed to get table info: #{e.message}"
|
145
|
+
{}
|
146
|
+
end
|
147
|
+
# rubocop:enable Metrics/MethodLength
|
148
|
+
|
149
|
+
# Collect schema information
|
150
|
+
#
|
151
|
+
# @return [Hash] schema statistics
|
152
|
+
# rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
153
|
+
def collect_schema_info
|
154
|
+
return {} unless active_record_available?
|
155
|
+
|
156
|
+
# Skip schema info if performance metrics are disabled
|
157
|
+
return {} unless Dbwatcher.configuration.system_info_include_performance_metrics
|
158
|
+
|
159
|
+
connection = ActiveRecord::Base.connection
|
160
|
+
schema_info = {}
|
161
|
+
|
162
|
+
# Get schema migrations if available
|
163
|
+
if connection.table_exists?("schema_migrations")
|
164
|
+
begin
|
165
|
+
versions = connection.select_values("SELECT version FROM schema_migrations ORDER BY version DESC")
|
166
|
+
schema_info[:migrations] = {
|
167
|
+
count: versions.size,
|
168
|
+
latest: versions.first
|
169
|
+
}
|
170
|
+
rescue StandardError => e
|
171
|
+
log_error "Failed to get schema migrations: #{e.message}"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Get schema information if available
|
176
|
+
if defined?(ActiveRecord::InternalMetadata) && connection.table_exists?(ActiveRecord::InternalMetadata.table_name)
|
177
|
+
begin
|
178
|
+
# Split the long line to avoid line length issues
|
179
|
+
query = "SELECT value FROM #{ActiveRecord::InternalMetadata.table_name} " \
|
180
|
+
"WHERE key = 'environment'"
|
181
|
+
environment = connection.select_value(query)
|
182
|
+
schema_info[:environment] = environment
|
183
|
+
rescue StandardError => e
|
184
|
+
log_error "Failed to get schema environment: #{e.message}"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
schema_info
|
189
|
+
rescue StandardError => e
|
190
|
+
log_error "Failed to get schema info: #{e.message}"
|
191
|
+
{}
|
192
|
+
end
|
193
|
+
# rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
194
|
+
|
195
|
+
# Collect index information
|
196
|
+
#
|
197
|
+
# @return [Hash] index statistics
|
198
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
199
|
+
def collect_index_info
|
200
|
+
return {} unless active_record_available?
|
201
|
+
|
202
|
+
# Skip index info if performance metrics are disabled
|
203
|
+
return {} unless Dbwatcher.configuration.system_info_include_performance_metrics
|
204
|
+
|
205
|
+
connection = ActiveRecord::Base.connection
|
206
|
+
tables = connection.tables
|
207
|
+
index_info = { count: 0, tables: [] }
|
208
|
+
|
209
|
+
tables.each do |table|
|
210
|
+
indexes = connection.indexes(table)
|
211
|
+
table_indexes = indexes.map do |index|
|
212
|
+
{
|
213
|
+
name: index.name,
|
214
|
+
columns: index.columns,
|
215
|
+
unique: index.unique
|
216
|
+
}
|
217
|
+
end
|
218
|
+
|
219
|
+
index_info[:tables] << {
|
220
|
+
name: table,
|
221
|
+
indexes: table_indexes
|
222
|
+
}
|
223
|
+
index_info[:count] += indexes.size
|
224
|
+
rescue StandardError => e
|
225
|
+
log_error "Failed to get indexes for table #{table}: #{e.message}"
|
226
|
+
index_info[:tables] << { name: table, error: e.message }
|
227
|
+
end
|
228
|
+
|
229
|
+
index_info
|
230
|
+
rescue StandardError => e
|
231
|
+
log_error "Failed to get index info: #{e.message}"
|
232
|
+
{}
|
233
|
+
end
|
234
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
235
|
+
|
236
|
+
# Collect query statistics if available
|
237
|
+
#
|
238
|
+
# @return [Hash] query statistics
|
239
|
+
def collect_query_stats
|
240
|
+
return {} unless active_record_available?
|
241
|
+
|
242
|
+
# This would be implementation-specific and could be expanded
|
243
|
+
# based on the database adapter and monitoring tools available
|
244
|
+
{}
|
245
|
+
rescue StandardError => e
|
246
|
+
log_error "Failed to get query stats: #{e.message}"
|
247
|
+
{}
|
248
|
+
end
|
249
|
+
|
250
|
+
# Check if ActiveRecord is available and connected
|
251
|
+
#
|
252
|
+
# @return [Boolean] true if ActiveRecord is available and connected
|
253
|
+
def active_record_available?
|
254
|
+
defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
|
255
|
+
rescue StandardError => e
|
256
|
+
log_error "Failed to check ActiveRecord availability: #{e.message}"
|
257
|
+
false
|
258
|
+
end
|
259
|
+
end
|
260
|
+
# rubocop:enable Metrics/ClassLength
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
@@ -0,0 +1,387 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "socket"
|
4
|
+
require "rbconfig"
|
5
|
+
require_relative "../../logging"
|
6
|
+
|
7
|
+
module Dbwatcher
|
8
|
+
module Services
|
9
|
+
module SystemInfo
|
10
|
+
# Machine information collector service
|
11
|
+
#
|
12
|
+
# Collects system-level information about the machine including CPU, memory,
|
13
|
+
# disk usage, and process information.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# info = SystemInfo::MachineInfoCollector.call
|
17
|
+
# puts info[:cpu][:model]
|
18
|
+
# puts info[:memory][:total]
|
19
|
+
# puts info[:disk][:total]
|
20
|
+
#
|
21
|
+
# This class is necessarily complex due to the comprehensive machine information
|
22
|
+
# it needs to collect across different operating systems.
|
23
|
+
# rubocop:disable Metrics/ClassLength
|
24
|
+
class MachineInfoCollector
|
25
|
+
include Dbwatcher::Logging
|
26
|
+
|
27
|
+
# Class method to create instance and call
|
28
|
+
#
|
29
|
+
# @return [Hash] machine information
|
30
|
+
def self.call
|
31
|
+
new.call
|
32
|
+
end
|
33
|
+
|
34
|
+
def call
|
35
|
+
log_info "#{self.class.name}: Collecting machine information"
|
36
|
+
|
37
|
+
{
|
38
|
+
hostname: collect_hostname,
|
39
|
+
os: collect_os_info,
|
40
|
+
cpu: collect_cpu_info,
|
41
|
+
memory: collect_memory_info,
|
42
|
+
disk: collect_disk_info,
|
43
|
+
process: collect_process_info,
|
44
|
+
load: collect_load_info,
|
45
|
+
uptime: collect_uptime
|
46
|
+
}
|
47
|
+
rescue StandardError => e
|
48
|
+
log_error "Machine info collection failed: #{e.message}"
|
49
|
+
{ error: e.message }
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Collect hostname information
|
55
|
+
#
|
56
|
+
# @return [String] hostname
|
57
|
+
def collect_hostname
|
58
|
+
Socket.gethostname
|
59
|
+
rescue StandardError => e
|
60
|
+
log_error "Failed to get hostname: #{e.message}"
|
61
|
+
"unknown"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Collect operating system information
|
65
|
+
#
|
66
|
+
# @return [Hash] operating system information
|
67
|
+
def collect_os_info
|
68
|
+
{
|
69
|
+
name: RbConfig::CONFIG["host_os"],
|
70
|
+
version: collect_os_version,
|
71
|
+
kernel: collect_kernel_version
|
72
|
+
}
|
73
|
+
rescue StandardError => e
|
74
|
+
log_error "Failed to get OS info: #{e.message}"
|
75
|
+
{ name: "unknown", version: "unknown", kernel: "unknown" }
|
76
|
+
end
|
77
|
+
|
78
|
+
# Collect operating system version
|
79
|
+
#
|
80
|
+
# @return [String] operating system version
|
81
|
+
# rubocop:disable Metrics/MethodLength
|
82
|
+
def collect_os_version
|
83
|
+
case RbConfig::CONFIG["host_os"]
|
84
|
+
when /darwin/
|
85
|
+
`sw_vers -productVersion`.strip
|
86
|
+
when /linux/
|
87
|
+
if File.exist?("/etc/os-release")
|
88
|
+
# Extract version from os-release file safely
|
89
|
+
os_release = File.read("/etc/os-release")
|
90
|
+
match = os_release.match(/VERSION="?([^"]+)"?/)
|
91
|
+
match ? match[1] : "unknown"
|
92
|
+
else
|
93
|
+
"unknown"
|
94
|
+
end
|
95
|
+
when /mswin|mingw/
|
96
|
+
`ver`.strip
|
97
|
+
else
|
98
|
+
"unknown"
|
99
|
+
end
|
100
|
+
rescue StandardError => e
|
101
|
+
log_error "Failed to get OS version: #{e.message}"
|
102
|
+
"unknown"
|
103
|
+
end
|
104
|
+
# rubocop:enable Metrics/MethodLength
|
105
|
+
|
106
|
+
# Collect kernel version
|
107
|
+
#
|
108
|
+
# @return [String] kernel version
|
109
|
+
def collect_kernel_version
|
110
|
+
`uname -r`.strip
|
111
|
+
rescue StandardError => e
|
112
|
+
log_error "Failed to get kernel version: #{e.message}"
|
113
|
+
"unknown"
|
114
|
+
end
|
115
|
+
|
116
|
+
# Collect CPU information
|
117
|
+
#
|
118
|
+
# @return [Hash] CPU information
|
119
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
120
|
+
def collect_cpu_info
|
121
|
+
cpu_info = {
|
122
|
+
model: "unknown",
|
123
|
+
architecture: RbConfig::CONFIG["host_cpu"],
|
124
|
+
cores: 1,
|
125
|
+
speed: "unknown",
|
126
|
+
load_average: [0.0, 0.0, 0.0]
|
127
|
+
}
|
128
|
+
|
129
|
+
# Try to get CPU model from /proc/cpuinfo on Linux
|
130
|
+
if File.exist?("/proc/cpuinfo")
|
131
|
+
cpuinfo = File.read("/proc/cpuinfo")
|
132
|
+
|
133
|
+
# Count cores
|
134
|
+
cpu_info[:cores] = cpuinfo.scan(/^processor\s*:/).length
|
135
|
+
|
136
|
+
# Get model name
|
137
|
+
model_line = cpuinfo.lines.grep(/^model name\s*:/).first
|
138
|
+
cpu_info[:model] = model_line.split(":", 2).last.strip if model_line
|
139
|
+
end
|
140
|
+
|
141
|
+
# Get load average
|
142
|
+
if File.exist?("/proc/loadavg")
|
143
|
+
loadavg = File.read("/proc/loadavg").strip.split
|
144
|
+
cpu_info[:load_average] = [
|
145
|
+
loadavg[0].to_f,
|
146
|
+
loadavg[1].to_f,
|
147
|
+
loadavg[2].to_f
|
148
|
+
]
|
149
|
+
end
|
150
|
+
|
151
|
+
cpu_info
|
152
|
+
rescue StandardError => e
|
153
|
+
log_error "Failed to get CPU info: #{e.message}"
|
154
|
+
{
|
155
|
+
model: "unknown",
|
156
|
+
architecture: "unknown",
|
157
|
+
cores: 1,
|
158
|
+
speed: "unknown",
|
159
|
+
load_average: [0.0, 0.0, 0.0]
|
160
|
+
}
|
161
|
+
end
|
162
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
163
|
+
|
164
|
+
# Collect memory information
|
165
|
+
#
|
166
|
+
# @return [Hash] memory information
|
167
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
168
|
+
def collect_memory_info
|
169
|
+
mem_info = {
|
170
|
+
total: 0,
|
171
|
+
free: 0,
|
172
|
+
available: 0,
|
173
|
+
used: 0
|
174
|
+
}
|
175
|
+
|
176
|
+
if File.exist?("/proc/meminfo")
|
177
|
+
mem_data = File.read("/proc/meminfo")
|
178
|
+
|
179
|
+
# Fix safe navigation chain length issues by breaking them up
|
180
|
+
match = mem_data.match(/MemTotal:\s+(\d+)/i)
|
181
|
+
total = match ? match.captures.first.to_i : 0
|
182
|
+
|
183
|
+
match = mem_data.match(/MemFree:\s+(\d+)/i)
|
184
|
+
free = match ? match.captures.first.to_i : 0
|
185
|
+
|
186
|
+
match = mem_data.match(/MemAvailable:\s+(\d+)/i)
|
187
|
+
available = match ? match.captures.first.to_i : 0
|
188
|
+
|
189
|
+
mem_info[:total] = total * 1024 # Convert KB to bytes
|
190
|
+
mem_info[:free] = free * 1024
|
191
|
+
mem_info[:available] = available * 1024
|
192
|
+
mem_info[:used] = mem_info[:total] - mem_info[:free]
|
193
|
+
else
|
194
|
+
# For non-Linux systems, try to get memory info from platform-specific commands
|
195
|
+
case RbConfig::CONFIG["host_os"]
|
196
|
+
when /darwin/
|
197
|
+
# macOS
|
198
|
+
begin
|
199
|
+
mem_data = `sysctl hw.memsize hw.physmem`.strip.split("\n")
|
200
|
+
total = mem_data.grep(/hw.memsize/).first&.split(":")&.last.to_i
|
201
|
+
mem_info[:total] = total if total&.positive?
|
202
|
+
rescue StandardError => e
|
203
|
+
log_error "Failed to get macOS memory info: #{e.message}"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
mem_info
|
209
|
+
rescue StandardError => e
|
210
|
+
log_error "Failed to get memory info: #{e.message}"
|
211
|
+
{ total: 0, free: 0, available: 0, used: 0 }
|
212
|
+
end
|
213
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
214
|
+
|
215
|
+
# Collect disk information
|
216
|
+
#
|
217
|
+
# @return [Hash] disk information
|
218
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
219
|
+
def collect_disk_info
|
220
|
+
disk_info = {
|
221
|
+
total: 0,
|
222
|
+
free: 0,
|
223
|
+
used: 0,
|
224
|
+
filesystems: []
|
225
|
+
}
|
226
|
+
|
227
|
+
# Use df command to get disk usage
|
228
|
+
begin
|
229
|
+
df_output = `df -k`.strip.split("\n")
|
230
|
+
df_output.shift # Remove header line
|
231
|
+
|
232
|
+
df_output.each do |line|
|
233
|
+
parts = line.split(/\s+/)
|
234
|
+
next if parts.size < 6 # Skip invalid lines
|
235
|
+
|
236
|
+
filesystem = {
|
237
|
+
device: parts[0],
|
238
|
+
mount_point: parts[5],
|
239
|
+
total: parts[1].to_i * 1024, # Convert KB to bytes
|
240
|
+
used: parts[2].to_i * 1024,
|
241
|
+
free: parts[3].to_i * 1024,
|
242
|
+
usage_percent: parts[4].to_i
|
243
|
+
}
|
244
|
+
|
245
|
+
disk_info[:filesystems] << filesystem
|
246
|
+
|
247
|
+
# Only count real filesystems (not special ones)
|
248
|
+
next if filesystem[:device].start_with?("tmpfs", "devtmpfs", "none")
|
249
|
+
|
250
|
+
disk_info[:total] += filesystem[:total]
|
251
|
+
disk_info[:used] += filesystem[:used]
|
252
|
+
disk_info[:free] += filesystem[:free]
|
253
|
+
end
|
254
|
+
rescue StandardError => e
|
255
|
+
log_error "Failed to get disk info: #{e.message}"
|
256
|
+
end
|
257
|
+
|
258
|
+
disk_info
|
259
|
+
rescue StandardError => e
|
260
|
+
log_error "Failed to get disk info: #{e.message}"
|
261
|
+
{ total: 0, free: 0, used: 0, filesystems: [] }
|
262
|
+
end
|
263
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
264
|
+
|
265
|
+
# Collect process information
|
266
|
+
#
|
267
|
+
# @return [Hash] process information
|
268
|
+
def collect_process_info
|
269
|
+
{
|
270
|
+
pid: Process.pid,
|
271
|
+
ppid: Process.ppid,
|
272
|
+
uid: Process.uid,
|
273
|
+
gid: Process.gid,
|
274
|
+
working_directory: Dir.pwd
|
275
|
+
}
|
276
|
+
rescue StandardError => e
|
277
|
+
log_error "Failed to get process info: #{e.message}"
|
278
|
+
{ pid: 0, ppid: 0, uid: 0, gid: 0, working_directory: "unknown" }
|
279
|
+
end
|
280
|
+
|
281
|
+
# Collect load average information
|
282
|
+
#
|
283
|
+
# @return [Hash] load average information
|
284
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
285
|
+
def collect_load_info
|
286
|
+
load_avg = [0.0, 0.0, 0.0]
|
287
|
+
load_info = {
|
288
|
+
"1min" => 0.0,
|
289
|
+
"5min" => 0.0,
|
290
|
+
"15min" => 0.0
|
291
|
+
}
|
292
|
+
|
293
|
+
# Try to get load average from /proc/loadavg
|
294
|
+
if File.exist?("/proc/loadavg")
|
295
|
+
begin
|
296
|
+
loadavg = File.read("/proc/loadavg").strip.split
|
297
|
+
load_avg = [loadavg[0].to_f, loadavg[1].to_f, loadavg[2].to_f]
|
298
|
+
rescue StandardError => e
|
299
|
+
log_error "Failed to read /proc/loadavg: #{e.message}"
|
300
|
+
end
|
301
|
+
else
|
302
|
+
# Try to get load average using uptime command
|
303
|
+
begin
|
304
|
+
uptime = `uptime`.strip
|
305
|
+
if uptime =~ /load average:?\s+([\d.]+),?\s+([\d.]+),?\s+([\d.]+)/
|
306
|
+
load_avg = [::Regexp.last_match(1).to_f, ::Regexp.last_match(2).to_f,
|
307
|
+
::Regexp.last_match(3).to_f]
|
308
|
+
end
|
309
|
+
rescue StandardError => e
|
310
|
+
log_error "Failed to get load average from uptime: #{e.message}"
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
load_info["1min"] = load_avg[0]
|
315
|
+
load_info["5min"] = load_avg[1]
|
316
|
+
load_info["15min"] = load_avg[2]
|
317
|
+
load_info
|
318
|
+
rescue StandardError => e
|
319
|
+
log_error "Failed to get load info: #{e.message}"
|
320
|
+
{ "1min" => 0.0, "5min" => 0.0, "15min" => 0.0 }
|
321
|
+
end
|
322
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
323
|
+
|
324
|
+
# Collect system uptime
|
325
|
+
#
|
326
|
+
# @return [Hash] uptime information
|
327
|
+
# rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
|
328
|
+
def collect_uptime
|
329
|
+
uptime_seconds = 0
|
330
|
+
uptime_info = {
|
331
|
+
seconds: 0,
|
332
|
+
formatted: "0 days, 0 hours, 0 minutes"
|
333
|
+
}
|
334
|
+
|
335
|
+
if File.exist?("/proc/uptime")
|
336
|
+
begin
|
337
|
+
uptime_seconds = File.read("/proc/uptime").strip.split.first.to_f
|
338
|
+
rescue StandardError => e
|
339
|
+
log_error "Failed to read /proc/uptime: #{e.message}"
|
340
|
+
end
|
341
|
+
else
|
342
|
+
# Try to get uptime using uptime command
|
343
|
+
begin
|
344
|
+
uptime_output = `uptime`.strip
|
345
|
+
if uptime_output =~ /up\s+(\d+)\s+days?,\s+(\d+):(\d+)/
|
346
|
+
days = ::Regexp.last_match(1).to_i
|
347
|
+
hours = ::Regexp.last_match(2).to_i
|
348
|
+
minutes = ::Regexp.last_match(3).to_i
|
349
|
+
uptime_seconds = (days * 86_400) + (hours * 3600) + (minutes * 60)
|
350
|
+
elsif uptime_output =~ /up\s+(\d+):(\d+)/
|
351
|
+
hours = ::Regexp.last_match(1).to_i
|
352
|
+
minutes = ::Regexp.last_match(2).to_i
|
353
|
+
uptime_seconds = (hours * 3600) + (minutes * 60)
|
354
|
+
end
|
355
|
+
rescue StandardError => e
|
356
|
+
log_error "Failed to get uptime from uptime command: #{e.message}"
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
uptime_info[:seconds] = uptime_seconds.to_i
|
361
|
+
uptime_info[:formatted] = format_uptime(uptime_seconds)
|
362
|
+
uptime_info
|
363
|
+
rescue StandardError => e
|
364
|
+
log_error "Failed to get uptime: #{e.message}"
|
365
|
+
{ seconds: 0, formatted: "0 days, 0 hours, 0 minutes" }
|
366
|
+
end
|
367
|
+
# rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
|
368
|
+
|
369
|
+
# Format uptime seconds into a human-readable string
|
370
|
+
#
|
371
|
+
# @param seconds [Float] uptime in seconds
|
372
|
+
# @return [String] formatted uptime
|
373
|
+
def format_uptime(seconds)
|
374
|
+
seconds = seconds.to_i
|
375
|
+
days = seconds / 86_400
|
376
|
+
seconds %= 86_400
|
377
|
+
hours = seconds / 3600
|
378
|
+
seconds %= 3600
|
379
|
+
minutes = seconds / 60
|
380
|
+
|
381
|
+
"#{days} days, #{hours} hours, #{minutes} minutes"
|
382
|
+
end
|
383
|
+
end
|
384
|
+
# rubocop:enable Metrics/ClassLength
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|