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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +24 -2
  3. data/app/assets/config/dbwatcher_manifest.js +1 -0
  4. data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +196 -119
  5. data/app/assets/javascripts/dbwatcher/components/dashboard.js +325 -0
  6. data/app/assets/javascripts/dbwatcher/components/timeline.js +211 -0
  7. data/app/assets/javascripts/dbwatcher/dbwatcher.js +5 -0
  8. data/app/assets/stylesheets/dbwatcher/application.css +691 -41
  9. data/app/assets/stylesheets/dbwatcher/application.scss +5 -0
  10. data/app/assets/stylesheets/dbwatcher/components/_badges.scss +68 -23
  11. data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +83 -26
  12. data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +3 -3
  13. data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +9 -0
  14. data/app/assets/stylesheets/dbwatcher/components/_tabulator.scss +248 -0
  15. data/app/assets/stylesheets/dbwatcher/components/_timeline.scss +326 -0
  16. data/app/assets/stylesheets/dbwatcher/vendor/_tabulator_overrides.scss +37 -0
  17. data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +18 -4
  18. data/app/controllers/dbwatcher/api/v1/system_info_controller.rb +180 -0
  19. data/app/controllers/dbwatcher/dashboard/system_info_controller.rb +64 -0
  20. data/app/controllers/dbwatcher/dashboard_controller.rb +17 -0
  21. data/app/controllers/dbwatcher/sessions_controller.rb +3 -19
  22. data/app/helpers/dbwatcher/application_helper.rb +43 -11
  23. data/app/helpers/dbwatcher/diagram_helper.rb +0 -88
  24. data/app/views/dbwatcher/dashboard/_layout.html.erb +27 -0
  25. data/app/views/dbwatcher/dashboard/_overview.html.erb +188 -0
  26. data/app/views/dbwatcher/dashboard/_system_info.html.erb +22 -0
  27. data/app/views/dbwatcher/dashboard/_system_info_content.html.erb +389 -0
  28. data/app/views/dbwatcher/dashboard/index.html.erb +8 -177
  29. data/app/views/dbwatcher/sessions/_layout.html.erb +26 -0
  30. data/app/views/dbwatcher/sessions/{_summary_tab.html.erb → _summary.html.erb} +1 -1
  31. data/app/views/dbwatcher/sessions/_tables.html.erb +170 -0
  32. data/app/views/dbwatcher/sessions/_timeline.html.erb +260 -0
  33. data/app/views/dbwatcher/sessions/index.html.erb +107 -87
  34. data/app/views/dbwatcher/sessions/show.html.erb +12 -4
  35. data/app/views/dbwatcher/tables/index.html.erb +32 -40
  36. data/app/views/layouts/dbwatcher/application.html.erb +101 -48
  37. data/config/routes.rb +25 -7
  38. data/lib/dbwatcher/configuration.rb +18 -1
  39. data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +102 -1
  40. data/lib/dbwatcher/services/api/{changes_data_service.rb → tables_data_service.rb} +6 -6
  41. data/lib/dbwatcher/services/base_service.rb +2 -0
  42. data/lib/dbwatcher/services/system_info/database_info_collector.rb +263 -0
  43. data/lib/dbwatcher/services/system_info/machine_info_collector.rb +387 -0
  44. data/lib/dbwatcher/services/system_info/runtime_info_collector.rb +328 -0
  45. data/lib/dbwatcher/services/system_info/system_info_collector.rb +114 -0
  46. data/lib/dbwatcher/services/timeline_data_service/enhancement_utilities.rb +100 -0
  47. data/lib/dbwatcher/services/timeline_data_service/entry_builder.rb +125 -0
  48. data/lib/dbwatcher/services/timeline_data_service/metadata_builder.rb +93 -0
  49. data/lib/dbwatcher/services/timeline_data_service.rb +130 -0
  50. data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +1 -1
  51. data/lib/dbwatcher/storage/concerns/error_handler.rb +6 -6
  52. data/lib/dbwatcher/storage/session.rb +5 -0
  53. data/lib/dbwatcher/storage/system_info_storage.rb +242 -0
  54. data/lib/dbwatcher/storage.rb +12 -0
  55. data/lib/dbwatcher/version.rb +1 -1
  56. data/lib/dbwatcher.rb +16 -2
  57. metadata +28 -16
  58. data/app/helpers/dbwatcher/component_helper.rb +0 -29
  59. data/app/views/dbwatcher/sessions/_changes_tab.html.erb +0 -265
  60. data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +0 -12
  61. data/app/views/dbwatcher/sessions/changes.html.erb +0 -21
  62. data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +0 -44
  63. data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +0 -96
  64. data/app/views/dbwatcher/sessions/diagrams.html.erb +0 -21
  65. data/app/views/dbwatcher/sessions/shared/_layout.html.erb +0 -8
  66. data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +0 -35
  67. data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +0 -25
  68. data/app/views/dbwatcher/sessions/summary.html.erb +0 -21
  69. /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