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,328 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../logging"
4
+
5
+ module Dbwatcher
6
+ module Services
7
+ module SystemInfo
8
+ # Runtime information collector service
9
+ #
10
+ # Collects runtime-specific information including Ruby version, Rails version,
11
+ # loaded gems, environment variables, and application configuration.
12
+ #
13
+ # @example
14
+ # info = SystemInfo::RuntimeInfoCollector.call
15
+ # puts info[:ruby_version]
16
+ # puts info[:rails_version]
17
+ # puts info[:gem_count]
18
+ #
19
+ # This class is necessarily complex due to the comprehensive runtime information
20
+ # it needs to collect across different Ruby and Rails environments.
21
+ # rubocop:disable Metrics/ClassLength
22
+ class RuntimeInfoCollector
23
+ include Dbwatcher::Logging
24
+
25
+ # Class method to create instance and call
26
+ #
27
+ # @return [Hash] runtime information
28
+ def self.call
29
+ new.call
30
+ end
31
+
32
+ # Collect runtime information
33
+ #
34
+ # @return [Hash] collected runtime information
35
+ # rubocop:disable Metrics/MethodLength
36
+ def call
37
+ log_info "#{self.class.name}: Collecting runtime information"
38
+
39
+ {
40
+ ruby_version: collect_ruby_version,
41
+ ruby_engine: collect_ruby_engine,
42
+ ruby_patchlevel: collect_ruby_patchlevel,
43
+ rails_version: collect_rails_version,
44
+ rails_env: collect_rails_env,
45
+ environment: collect_environment,
46
+ pid: Process.pid,
47
+ gem_count: collect_gem_count,
48
+ loaded_gems: collect_loaded_gems,
49
+ load_path: collect_load_path_info,
50
+ environment_variables: collect_environment_variables,
51
+ application_config: collect_application_config
52
+ }
53
+ rescue StandardError => e
54
+ log_error "Runtime info collection failed: #{e.message}"
55
+ { error: e.message }
56
+ end
57
+ # rubocop:enable Metrics/MethodLength
58
+
59
+ private
60
+
61
+ # Collect Ruby version information
62
+ #
63
+ # @return [String] Ruby version
64
+ def collect_ruby_version
65
+ RUBY_VERSION
66
+ rescue StandardError => e
67
+ log_error "Failed to get Ruby version: #{e.message}"
68
+ "unknown"
69
+ end
70
+
71
+ # Collect Ruby engine information
72
+ #
73
+ # @return [String] Ruby engine (e.g., ruby, jruby, rbx)
74
+ def collect_ruby_engine
75
+ defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby"
76
+ rescue StandardError => e
77
+ log_error "Failed to get Ruby engine: #{e.message}"
78
+ "unknown"
79
+ end
80
+
81
+ # Collect Ruby patchlevel
82
+ #
83
+ # @return [String] Ruby patchlevel
84
+ def collect_ruby_patchlevel
85
+ RUBY_PATCHLEVEL.to_s
86
+ rescue StandardError => e
87
+ log_error "Failed to get Ruby patchlevel: #{e.message}"
88
+ "unknown"
89
+ end
90
+
91
+ # Collect Rails version if available
92
+ #
93
+ # @return [String] Rails version or nil
94
+ def collect_rails_version
95
+ return nil unless defined?(Rails)
96
+
97
+ Rails.version
98
+ rescue StandardError => e
99
+ log_error "Failed to get Rails version: #{e.message}"
100
+ nil
101
+ end
102
+
103
+ # Collect Rails environment if available
104
+ #
105
+ # @return [String] Rails environment or nil
106
+ def collect_rails_env
107
+ return nil unless defined?(Rails)
108
+
109
+ Rails.env
110
+ rescue StandardError => e
111
+ log_error "Failed to get Rails environment: #{e.message}"
112
+ nil
113
+ end
114
+
115
+ # Collect current environment
116
+ #
117
+ # @return [String] current environment
118
+ def collect_environment
119
+ ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
120
+ rescue StandardError => e
121
+ log_error "Failed to get environment: #{e.message}"
122
+ "unknown"
123
+ end
124
+
125
+ # Collect loaded gems count
126
+ #
127
+ # @return [Integer] number of loaded gems
128
+ def collect_gem_count
129
+ Gem.loaded_specs.size
130
+ rescue StandardError => e
131
+ log_error "Failed to get gem count: #{e.message}"
132
+ 0
133
+ end
134
+
135
+ # Collect loaded gems information
136
+ #
137
+ # @return [Hash] loaded gems with versions
138
+ def collect_loaded_gems
139
+ return {} unless Dbwatcher.configuration.system_info_include_performance_metrics
140
+
141
+ gems = {}
142
+ Gem.loaded_specs.each do |name, spec|
143
+ gems[name] = spec.version.to_s
144
+ end
145
+ gems
146
+ rescue StandardError => e
147
+ log_error "Failed to get loaded gems: #{e.message}"
148
+ {}
149
+ end
150
+
151
+ # Collect load path information
152
+ #
153
+ # @return [Hash] load path statistics
154
+ def collect_load_path_info
155
+ {
156
+ size: $LOAD_PATH.size,
157
+ paths: Dbwatcher.configuration.system_info_include_performance_metrics ? $LOAD_PATH.first(10) : []
158
+ }
159
+ rescue StandardError => e
160
+ log_error "Failed to get load path info: #{e.message}"
161
+ { size: 0, paths: [] }
162
+ end
163
+
164
+ # Collect environment variables (filtered for security)
165
+ #
166
+ # @return [Hash] filtered environment variables
167
+ # rubocop:disable Metrics/MethodLength
168
+ def collect_environment_variables
169
+ return {} unless Dbwatcher.configuration.collect_sensitive_env_vars
170
+
171
+ env_vars = {}
172
+
173
+ # Safe environment variables to include
174
+ safe_vars = %w[
175
+ RAILS_ENV
176
+ RACK_ENV
177
+ RUBY_VERSION
178
+ PATH
179
+ HOME
180
+ USER
181
+ SHELL
182
+ TERM
183
+ LANG
184
+ LC_ALL
185
+ TZ
186
+ RAILS_LOG_LEVEL
187
+ RAILS_SERVE_STATIC_FILES
188
+ RAILS_CACHE_ID
189
+ RAILS_RELATIVE_URL_ROOT
190
+ BUNDLE_GEMFILE
191
+ BUNDLE_PATH
192
+ GEM_HOME
193
+ GEM_PATH
194
+ ]
195
+
196
+ safe_vars.each do |var|
197
+ env_vars[var] = ENV[var] if ENV[var]
198
+ end
199
+
200
+ env_vars
201
+ rescue StandardError => e
202
+ log_error "Failed to get environment variables: #{e.message}"
203
+ {}
204
+ end
205
+ # rubocop:enable Metrics/MethodLength
206
+
207
+ # Collect application configuration
208
+ #
209
+ # @return [Hash] application configuration
210
+ def collect_application_config
211
+ config = {}
212
+
213
+ # Collect Rails configuration if available
214
+ config[:rails] = collect_rails_config if defined?(Rails)
215
+
216
+ # Collect DBWatcher configuration
217
+ config[:dbwatcher] = collect_dbwatcher_config if defined?(Dbwatcher)
218
+
219
+ config
220
+ rescue StandardError => e
221
+ log_error "Failed to get application config: #{e.message}"
222
+ {}
223
+ end
224
+
225
+ # Collect Rails configuration if available
226
+ #
227
+ # @return [Hash] Rails configuration
228
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
229
+ def collect_rails_config
230
+ rails_config = {}
231
+
232
+ return rails_config unless defined?(Rails)
233
+
234
+ # Basic Rails configuration
235
+ rails_config[:environment] = Rails.env
236
+ rails_config[:version] = Rails.version
237
+ rails_config[:root] = Rails.root.to_s
238
+
239
+ # Rails application configuration
240
+ if Rails.application
241
+ app_config = Rails.application.config
242
+ rails_config[:app_name] = Rails.application.class.name.split("::").first
243
+ rails_config[:autoload_paths] =
244
+ app_config.respond_to?(:autoload_paths) ? app_config.autoload_paths.size : nil
245
+ rails_config[:eager_load_paths] =
246
+ app_config.respond_to?(:eager_load_paths) ? app_config.eager_load_paths.size : nil
247
+ rails_config[:eager_load] = app_config.respond_to?(:eager_load) ? app_config.eager_load : nil
248
+ rails_config[:cache_classes] = app_config.respond_to?(:cache_classes) ? app_config.cache_classes : nil
249
+ rails_config[:consider_all_requests_local] =
250
+ app_config.respond_to?(:consider_all_requests_local) ? app_config.consider_all_requests_local : nil
251
+
252
+ # Fix long line by breaking it up and removing redundant else
253
+ perform_caching = if app_config.action_controller.respond_to?(:perform_caching)
254
+ app_config.action_controller.perform_caching
255
+ end
256
+ rails_config[:action_controller] = { perform_caching: perform_caching }
257
+ end
258
+
259
+ rails_config
260
+ rescue StandardError => e
261
+ log_error "Failed to get Rails config: #{e.message}"
262
+ {}
263
+ end
264
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
265
+
266
+ # Collect DBWatcher configuration
267
+ #
268
+ # @return [Hash] DBWatcher configuration
269
+ # rubocop:disable Metrics/MethodLength
270
+ def collect_dbwatcher_config
271
+ config = Dbwatcher.configuration
272
+
273
+ # Only include non-sensitive configuration options
274
+ # Split long lines to avoid line length issues
275
+ {
276
+ system_info_refresh_interval:
277
+ if config.respond_to?(:system_info_refresh_interval)
278
+ config.system_info_refresh_interval
279
+ else
280
+ 300
281
+ end,
282
+ collect_sensitive_env_vars:
283
+ if config.respond_to?(:collect_sensitive_env_vars)
284
+ config.collect_sensitive_env_vars
285
+ else
286
+ false
287
+ end,
288
+ system_info_include_performance_metrics:
289
+ if config.respond_to?(:system_info_include_performance_metrics)
290
+ config.system_info_include_performance_metrics
291
+ else
292
+ true
293
+ end
294
+ }
295
+ rescue StandardError => e
296
+ log_error "Failed to get DBWatcher config: #{e.message}"
297
+ {}
298
+ end
299
+ # rubocop:enable Metrics/MethodLength
300
+
301
+ # Sanitize database configuration to remove sensitive information
302
+ #
303
+ # @param db_config [Hash] database configuration
304
+ # @return [Hash] sanitized database configuration
305
+ def sanitize_database_config(db_config)
306
+ return {} unless db_config.is_a?(Hash)
307
+
308
+ # Create a copy to avoid modifying the original
309
+ sanitized = db_config.dup
310
+
311
+ # Remove sensitive information
312
+ %w[password username user].each do |key|
313
+ sanitized.delete(key)
314
+ sanitized.delete(key.to_sym)
315
+ end
316
+
317
+ # Keep only basic connection info
318
+ safe_keys = %w[adapter host port database pool timeout]
319
+ sanitized.select { |k, _| safe_keys.include?(k.to_s) }
320
+ rescue StandardError => e
321
+ log_error "Failed to sanitize database config: #{e.message}"
322
+ {}
323
+ end
324
+ end
325
+ # rubocop:enable Metrics/ClassLength
326
+ end
327
+ end
328
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "machine_info_collector"
4
+ require_relative "database_info_collector"
5
+ require_relative "runtime_info_collector"
6
+ require_relative "../../logging"
7
+
8
+ module Dbwatcher
9
+ module Services
10
+ module SystemInfo
11
+ # Main system information collector service
12
+ #
13
+ # Orchestrates the collection of system information from various sources
14
+ # including machine, database, and runtime information.
15
+ #
16
+ # @example
17
+ # info = SystemInfo::SystemInfoCollector.call
18
+ # puts info[:machine][:hostname]
19
+ # puts info[:database][:adapter]
20
+ # puts info[:runtime][:ruby_version]
21
+ class SystemInfoCollector
22
+ include Dbwatcher::Logging
23
+
24
+ # Class method to create instance and call
25
+ #
26
+ # @return [Hash] system information
27
+ def self.call
28
+ new.call
29
+ end
30
+
31
+ # Collect system information from all sources
32
+ #
33
+ # This method needs to be longer to properly handle all the collection
34
+ # steps and error handling in a consistent way.
35
+ #
36
+ # @return [Hash] collected system information
37
+ # rubocop:disable Metrics/MethodLength
38
+ def call
39
+ start_time = current_time
40
+ log_info "#{self.class.name}: Starting system information collection"
41
+
42
+ info = {
43
+ machine: collect_machine_info,
44
+ database: collect_database_info,
45
+ runtime: collect_runtime_info,
46
+ collected_at: current_time.iso8601,
47
+ collection_duration: nil
48
+ }
49
+
50
+ info[:collection_duration] = (current_time - start_time).round(3)
51
+ log_info "#{self.class.name}: Completed system information collection in #{info[:collection_duration]}s"
52
+
53
+ info
54
+ rescue StandardError => e
55
+ log_error "System information collection failed: #{e.message}"
56
+ {
57
+ machine: {},
58
+ database: {},
59
+ runtime: {},
60
+ collected_at: current_time.iso8601,
61
+ collection_duration: nil,
62
+ error: e.message
63
+ }
64
+ end
65
+ # rubocop:enable Metrics/MethodLength
66
+
67
+ private
68
+
69
+ # Get current time, using Rails Time.current if available, otherwise Time.now
70
+ #
71
+ # @return [Time] current time
72
+ def current_time
73
+ defined?(Time.current) ? Time.current : Time.now
74
+ end
75
+
76
+ # Collect machine information safely
77
+ #
78
+ # @return [Hash] machine information or empty hash on error
79
+ def collect_machine_info
80
+ return {} unless Dbwatcher.configuration.collect_system_info
81
+
82
+ MachineInfoCollector.call
83
+ rescue StandardError => e
84
+ log_error "Machine info collection failed: #{e.message}"
85
+ { error: e.message }
86
+ end
87
+
88
+ # Collect database information safely
89
+ #
90
+ # @return [Hash] database information or empty hash on error
91
+ def collect_database_info
92
+ return {} unless Dbwatcher.configuration.collect_system_info
93
+
94
+ DatabaseInfoCollector.call
95
+ rescue StandardError => e
96
+ log_error "Database info collection failed: #{e.message}"
97
+ { error: e.message }
98
+ end
99
+
100
+ # Collect runtime information safely
101
+ #
102
+ # @return [Hash] runtime information or empty hash on error
103
+ def collect_runtime_info
104
+ return {} unless Dbwatcher.configuration.collect_system_info
105
+
106
+ RuntimeInfoCollector.call
107
+ rescue StandardError => e
108
+ log_error "Runtime info collection failed: #{e.message}"
109
+ { error: e.message }
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -28,16 +28,16 @@ module Dbwatcher
28
28
  def safe_operation(operation_name, default_value = nil, &block)
29
29
  block.call
30
30
  rescue JSON::ParserError => e
31
- log_error("JSON parsing failed in #{operation_name}", e)
31
+ log_error_with_exception("JSON parsing failed in #{operation_name}", e)
32
32
  default_value
33
33
  rescue Errno::ENOENT => e
34
- log_error("File not found in #{operation_name}", e)
34
+ log_error_with_exception("File not found in #{operation_name}", e)
35
35
  default_value
36
36
  rescue Errno::EACCES => e
37
- log_error("Permission denied in #{operation_name}", e)
37
+ log_error_with_exception("Permission denied in #{operation_name}", e)
38
38
  raise StorageError, "Permission denied: #{e.message}"
39
39
  rescue StandardError => e
40
- log_error("#{operation_name} failed", e)
40
+ log_error_with_exception("#{operation_name} failed", e)
41
41
  default_value
42
42
  end
43
43
 
@@ -51,7 +51,7 @@ module Dbwatcher
51
51
  block.call
52
52
  rescue StandardError => e
53
53
  error_message = "Storage #{operation} failed: #{e.message}"
54
- log_error(error_message, e)
54
+ log_error_with_exception(error_message, e)
55
55
  raise StorageError, error_message
56
56
  end
57
57
 
@@ -62,7 +62,7 @@ module Dbwatcher
62
62
  # @param message [String] the error message
63
63
  # @param error [Exception] the exception that occurred
64
64
  # @return [void]
65
- def log_error(message, error)
65
+ def log_error_with_exception(message, error)
66
66
  if defined?(Rails) && Rails.logger
67
67
  Rails.logger.warn("#{message}: #{error.message}")
68
68
  else
@@ -28,6 +28,11 @@ module Dbwatcher
28
28
  }
29
29
  end
30
30
 
31
+ # Used by Rails URL helpers to convert the object to a URL parameter
32
+ def to_param
33
+ id.to_s
34
+ end
35
+
31
36
  def summary
32
37
  return {} unless changes.is_a?(Array)
33
38