dbwatcher 1.1.1 → 1.1.2

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +24 -2
  3. data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +12 -22
  4. data/app/assets/javascripts/dbwatcher/components/dashboard.js +325 -0
  5. data/app/assets/stylesheets/dbwatcher/application.css +394 -41
  6. data/app/assets/stylesheets/dbwatcher/application.scss +4 -0
  7. data/app/assets/stylesheets/dbwatcher/components/_badges.scss +68 -23
  8. data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +83 -26
  9. data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +3 -3
  10. data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +9 -0
  11. data/app/assets/stylesheets/dbwatcher/components/_tabulator.scss +248 -0
  12. data/app/assets/stylesheets/dbwatcher/vendor/_tabulator_overrides.scss +37 -0
  13. data/app/controllers/dbwatcher/api/v1/system_info_controller.rb +180 -0
  14. data/app/controllers/dbwatcher/dashboard/system_info_controller.rb +64 -0
  15. data/app/controllers/dbwatcher/dashboard_controller.rb +17 -0
  16. data/app/controllers/dbwatcher/sessions_controller.rb +3 -19
  17. data/app/helpers/dbwatcher/application_helper.rb +43 -11
  18. data/app/helpers/dbwatcher/diagram_helper.rb +0 -88
  19. data/app/views/dbwatcher/dashboard/_layout.html.erb +27 -0
  20. data/app/views/dbwatcher/dashboard/_overview.html.erb +188 -0
  21. data/app/views/dbwatcher/dashboard/_system_info.html.erb +22 -0
  22. data/app/views/dbwatcher/dashboard/_system_info_content.html.erb +389 -0
  23. data/app/views/dbwatcher/dashboard/index.html.erb +8 -177
  24. data/app/views/dbwatcher/sessions/_changes.html.erb +91 -0
  25. data/app/views/dbwatcher/sessions/_layout.html.erb +23 -0
  26. data/app/views/dbwatcher/sessions/index.html.erb +107 -87
  27. data/app/views/dbwatcher/sessions/show.html.erb +10 -4
  28. data/app/views/dbwatcher/tables/index.html.erb +32 -40
  29. data/app/views/layouts/dbwatcher/application.html.erb +100 -48
  30. data/config/routes.rb +23 -6
  31. data/lib/dbwatcher/configuration.rb +18 -1
  32. data/lib/dbwatcher/services/base_service.rb +2 -0
  33. data/lib/dbwatcher/services/system_info/database_info_collector.rb +263 -0
  34. data/lib/dbwatcher/services/system_info/machine_info_collector.rb +387 -0
  35. data/lib/dbwatcher/services/system_info/runtime_info_collector.rb +328 -0
  36. data/lib/dbwatcher/services/system_info/system_info_collector.rb +114 -0
  37. data/lib/dbwatcher/storage/concerns/error_handler.rb +6 -6
  38. data/lib/dbwatcher/storage/session.rb +5 -0
  39. data/lib/dbwatcher/storage/system_info_storage.rb +242 -0
  40. data/lib/dbwatcher/storage.rb +12 -0
  41. data/lib/dbwatcher/version.rb +1 -1
  42. data/lib/dbwatcher.rb +15 -1
  43. metadata +20 -15
  44. data/app/helpers/dbwatcher/component_helper.rb +0 -29
  45. data/app/views/dbwatcher/sessions/_changes_tab.html.erb +0 -265
  46. data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +0 -12
  47. data/app/views/dbwatcher/sessions/changes.html.erb +0 -21
  48. data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +0 -44
  49. data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +0 -96
  50. data/app/views/dbwatcher/sessions/diagrams.html.erb +0 -21
  51. data/app/views/dbwatcher/sessions/shared/_layout.html.erb +0 -8
  52. data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +0 -35
  53. data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +0 -25
  54. data/app/views/dbwatcher/sessions/summary.html.erb +0 -21
  55. /data/app/views/dbwatcher/sessions/{_diagrams_tab.html.erb → _diagrams.html.erb} +0 -0
  56. /data/app/views/dbwatcher/sessions/{_summary_tab.html.erb → _summary.html.erb} +0 -0
@@ -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