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
@@ -29,6 +29,7 @@
29
29
  <%= javascript_include_tag "dbwatcher/components/changes_table_hybrid" %>
30
30
  <%= javascript_include_tag "dbwatcher/components/diagrams" %>
31
31
  <%= javascript_include_tag "dbwatcher/components/summary" %>
32
+ <%= javascript_include_tag "dbwatcher/components/dashboard" %>
32
33
 
33
34
  <!-- DBWatcher Services -->
34
35
  <%= javascript_include_tag "dbwatcher/core/alpine_store" %>
@@ -56,9 +57,9 @@
56
57
  }
57
58
  });
58
59
 
59
- // Fallback initialization with better error handling
60
+ // Improved initialization with safety checks
60
61
  document.addEventListener('DOMContentLoaded', function() {
61
- // Small delay to ensure Alpine is available
62
+ // Prevent immediate DOM access conflicts
62
63
  setTimeout(() => {
63
64
  try {
64
65
  if (window.DBWatcher && !DBWatcher.initialized) {
@@ -68,16 +69,22 @@
68
69
  } catch (error) {
69
70
  console.error('❌ Error during DBWatcher fallback initialization:', error);
70
71
  }
71
- }, 100);
72
+ }, 150);
72
73
 
73
- // Plugin verification
74
+ // Plugin verification with safety checks
74
75
  setTimeout(() => {
75
- if (window.Alpine && !window.Alpine.directive('collapse')) {
76
- console.warn('⚠️ Alpine.js Collapse plugin may not be properly loaded');
77
- } else if (window.Alpine && window.Alpine.directive('collapse')) {
78
- console.log(' Alpine.js Collapse plugin verified');
76
+ try {
77
+ if (window.Alpine && typeof window.Alpine.directive === 'function') {
78
+ if (!window.Alpine.directive('collapse')) {
79
+ console.warn('⚠️ Alpine.js Collapse plugin may not be properly loaded');
80
+ } else {
81
+ console.log('✅ Alpine.js Collapse plugin verified');
82
+ }
83
+ }
84
+ } catch (error) {
85
+ console.warn('⚠️ Alpine.js plugin verification failed:', error);
79
86
  }
80
- }, 200);
87
+ }, 300);
81
88
  });
82
89
  </script>
83
90
 
@@ -129,7 +136,7 @@
129
136
 
130
137
  <!-- Navigation -->
131
138
  <nav class="flex-1 py-2 overflow-y-auto">
132
- <%= link_to root_path, class: "sidebar-item #{current_page?(root_path) ? 'active' : ''}" do %>
139
+ <%= link_to root_path, class: "sidebar-item #{current_page?(root_path) || (params[:tab] == 'system_info') ? 'active' : ''}" do %>
133
140
  <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
134
141
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
135
142
  d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"/>
@@ -160,6 +167,8 @@
160
167
  </svg>
161
168
  <span x-show="!sidebarCollapsed">SQL Logs</span>
162
169
  <% end %>
170
+
171
+
163
172
  </nav>
164
173
 
165
174
  <!-- Actions -->
@@ -176,6 +185,42 @@
176
185
  <span x-show="!sidebarCollapsed" class="text-xs">Clear All</span>
177
186
  <% end %>
178
187
  </div>
188
+
189
+ <!-- Gem Info Section -->
190
+ <div class="mt-auto pt-4 border-t border-gray-700">
191
+ <div class="px-3 py-2">
192
+ <div class="text-xs text-gray-400 mb-2 font-medium" x-show="!sidebarCollapsed">
193
+ dbwatcher v<%= Dbwatcher::VERSION %>
194
+ </div>
195
+ <div class="flex items-center gap-2 text-xs text-gray-400" x-show="!sidebarCollapsed">
196
+ <a href="https://github.com/patrick204nqh/dbwatcher"
197
+ target="_blank"
198
+ rel="noopener noreferrer"
199
+ class="hover:text-blue-400 transition-colors duration-200 flex items-center gap-1"
200
+ title="View on GitHub">
201
+ <svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24">
202
+ <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
203
+ </svg>
204
+ <span>GitHub</span>
205
+ </a>
206
+ <span class="text-gray-600">•</span>
207
+ <a href="https://rubydoc.info/gems/dbwatcher/<%= Dbwatcher::VERSION %>"
208
+ target="_blank"
209
+ rel="noopener noreferrer"
210
+ class="hover:text-red-400 transition-colors duration-200 flex items-center gap-1"
211
+ title="View Documentation">
212
+ <svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24">
213
+ <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
214
+ </svg>
215
+ <span>Docs</span>
216
+ </a>
217
+ </div>
218
+ <!-- Collapsed state - just show version -->
219
+ <div class="text-xs text-gray-400 text-center" x-show="sidebarCollapsed" title="DBWatcher v<%= Dbwatcher::VERSION %>">
220
+ v<%= Dbwatcher::VERSION %>
221
+ </div>
222
+ </div>
223
+ </div>
179
224
  </div>
180
225
  </aside>
181
226
 
@@ -211,50 +256,57 @@
211
256
  <!-- Initialize DBWatcher system -->
212
257
  <script>
213
258
  document.addEventListener('DOMContentLoaded', function() {
214
- // Initialize DBWatcher if available
215
- if (window.DBWatcher) {
216
- // Ensure BaseComponent is available
217
- if (!window.DBWatcher.BaseComponent && typeof DBWatcher.BaseComponent !== 'function') {
218
- window.DBWatcher.BaseComponent = function(config = {}) {
219
- return {
220
- init() {
221
- if (this.componentInit) {
222
- try {
223
- this.componentInit();
224
- } catch (error) {
225
- console.error("Error during component initialization:", error);
259
+ // Add delay to prevent timing conflicts
260
+ setTimeout(() => {
261
+ try {
262
+ // Initialize DBWatcher if available
263
+ if (window.DBWatcher) {
264
+ // Ensure BaseComponent is available
265
+ if (!window.DBWatcher.BaseComponent && typeof DBWatcher.BaseComponent !== 'function') {
266
+ window.DBWatcher.BaseComponent = function(config = {}) {
267
+ return {
268
+ init() {
269
+ if (this.componentInit) {
270
+ try {
271
+ this.componentInit();
272
+ } catch (error) {
273
+ console.error("Error during component initialization:", error);
274
+ }
275
+ }
226
276
  }
277
+ };
278
+ };
279
+ console.log('Added fallback BaseComponent');
280
+ }
281
+
282
+ // Register legacy components with Alpine directly if ComponentRegistry isn't available
283
+ if (!window.DBWatcher.ComponentRegistry) {
284
+ console.log('ComponentRegistry not available, using direct Alpine registration');
285
+
286
+ // Ensure Alpine is available with safety checks
287
+ if (window.Alpine && typeof window.Alpine.data === 'function') {
288
+ // Direct registration of components with Alpine
289
+ if (!window.Alpine.data('changesTable') && window.DBWatcher.components && window.DBWatcher.components.changesTable) {
290
+ window.Alpine.data('changesTable', (config = {}) => {
291
+ const component = window.DBWatcher.components.changesTable(config);
292
+ return component;
293
+ });
294
+ console.log('Registered changesTable component with Alpine');
227
295
  }
228
296
  }
229
- };
230
- };
231
- console.log('Added fallback BaseComponent');
232
- }
233
-
234
- // Register legacy components with Alpine directly if ComponentRegistry isn't available
235
- if (!window.DBWatcher.ComponentRegistry) {
236
- console.log('ComponentRegistry not available, using direct Alpine registration');
237
-
238
- // Ensure Alpine is available
239
- if (window.Alpine) {
240
- // Direct registration of components with Alpine
241
- if (!window.Alpine.data('changesTable') && window.DBWatcher.components && window.DBWatcher.components.changesTable) {
242
- window.Alpine.data('changesTable', (config = {}) => {
243
- const component = window.DBWatcher.components.changesTable(config);
244
- return component;
245
- });
246
- console.log('Registered changesTable component with Alpine');
247
297
  }
248
- }
249
- }
250
298
 
251
- console.log('✅ DBWatcher initialized (fallback)');
252
- }
299
+ console.log('✅ DBWatcher initialized (fallback)');
300
+ }
253
301
 
254
- // Verify Alpine.js plugins
255
- if (window.Alpine && window.Alpine.directive && window.Alpine.directive('collapse')) {
256
- console.log('✅ Alpine.js Collapse plugin verified');
257
- }
302
+ // Verify Alpine.js plugins with safety checks
303
+ if (window.Alpine && typeof window.Alpine.directive === 'function' && window.Alpine.directive('collapse')) {
304
+ console.log('✅ Alpine.js Collapse plugin verified');
305
+ }
306
+ } catch (error) {
307
+ console.error('❌ Error during DBWatcher system initialization:', error);
308
+ }
309
+ }, 200);
258
310
  });
259
311
  </script>
260
312
  </body>
data/config/routes.rb CHANGED
@@ -3,16 +3,20 @@
3
3
  Dbwatcher::Engine.routes.draw do
4
4
  root to: "dashboard#index"
5
5
 
6
- # Dashboard clear all action
6
+ # Dashboard actions
7
7
  delete :clear_all, to: "dashboard#clear_all"
8
8
 
9
- resources :sessions do
10
- member do
11
- get :changes
12
- get :summary
13
- get :diagrams
9
+ # Dashboard system info actions
10
+ namespace :dashboard do
11
+ resources :system_info, only: [] do
12
+ collection do
13
+ post :refresh
14
+ delete :clear_cache
15
+ end
14
16
  end
17
+ end
15
18
 
19
+ resources :sessions do
16
20
  collection do
17
21
  delete :clear
18
22
  end
@@ -32,6 +36,19 @@ Dbwatcher::Engine.routes.draw do
32
36
  get :diagram_types
33
37
  end
34
38
  end
39
+
40
+ # System information API routes
41
+ resources :system_info, only: [:index] do
42
+ collection do
43
+ post :refresh
44
+ get :machine
45
+ get :database
46
+ get :runtime
47
+ get :summary
48
+ delete :clear_cache
49
+ get :cache_status
50
+ end
51
+ end
35
52
  end
36
53
  end
37
54
 
@@ -21,6 +21,11 @@ module Dbwatcher
21
21
  :diagram_preserve_table_case, :diagram_direction, :diagram_cardinality_format,
22
22
  :diagram_show_attribute_count, :diagram_show_method_count
23
23
 
24
+ # System information configuration
25
+ attr_accessor :collect_system_info, :system_info_refresh_interval,
26
+ :collect_sensitive_env_vars, :system_info_cache_duration,
27
+ :system_info_include_performance_metrics
28
+
24
29
  # Initialize with default values
25
30
  def initialize
26
31
  # Storage configuration defaults
@@ -30,7 +35,7 @@ module Dbwatcher
30
35
  @auto_clean_after_days = 7
31
36
 
32
37
  # Query tracking configuration defaults
33
- @track_queries = true
38
+ @track_queries = false
34
39
  @slow_query_threshold = 200 # milliseconds
35
40
  @max_query_logs_per_day = 1000
36
41
 
@@ -39,6 +44,9 @@ module Dbwatcher
39
44
 
40
45
  # Initialize diagram configuration with defaults
41
46
  initialize_diagram_config
47
+
48
+ # Initialize system information configuration with defaults
49
+ initialize_system_info_config
42
50
  end
43
51
 
44
52
  # Initialize diagram configuration with default values
@@ -56,6 +64,15 @@ module Dbwatcher
56
64
  @diagram_show_method_count = true
57
65
  end
58
66
 
67
+ # Initialize system information configuration with default values
68
+ def initialize_system_info_config
69
+ @collect_system_info = true
70
+ @system_info_refresh_interval = 5 * 60 # 5 minutes in seconds
71
+ @collect_sensitive_env_vars = false
72
+ @system_info_cache_duration = 60 * 60 # 1 hour in seconds
73
+ @system_info_include_performance_metrics = true
74
+ end
75
+
59
76
  # Validate configuration
60
77
  #
61
78
  # @return [Boolean] true if configuration is valid
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../logging"
4
+
3
5
  module Dbwatcher
4
6
  module Services
5
7
  # Base class for all service objects
@@ -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