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,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../services/system_info/system_info_collector"
4
+ require_relative "../logging"
5
+
6
+ module Dbwatcher
7
+ module Storage
8
+ # System information storage class
9
+ #
10
+ # Handles storage, caching, and retrieval of system information data.
11
+ # Provides intelligent caching with configurable TTL and refresh capabilities.
12
+ #
13
+ # @example
14
+ # storage = SystemInfoStorage.new
15
+ # info = storage.cached_info
16
+ # storage.refresh_info
17
+ #
18
+ # This class is necessarily complex due to the comprehensive system information
19
+ # storage and retrieval functionality it provides.
20
+ # rubocop:disable Metrics/ClassLength
21
+ class SystemInfoStorage < BaseStorage
22
+ include Dbwatcher::Logging
23
+ # Initialize system info storage
24
+ def initialize
25
+ super
26
+ @info_file = File.join(storage_path, "system_info.json")
27
+ end
28
+
29
+ # Save system information to storage
30
+ #
31
+ # @param info [Hash] system information data
32
+ # @return [Boolean] true if successful
33
+ def save_info(info)
34
+ # Convert all keys to strings before saving to ensure consistent format
35
+ info_with_string_keys = convert_keys_to_strings(info)
36
+ safe_write_json(@info_file, info_with_string_keys)
37
+ end
38
+
39
+ # Load system information from storage
40
+ #
41
+ # @return [Hash] system information data or empty hash
42
+ def load_info
43
+ # Override the base class default to return {} instead of []
44
+ safe_operation("read JSON from #{@info_file}", {}) do
45
+ result = file_manager.read_json(@info_file)
46
+ result = {} if result.is_a?(Array) && result.empty?
47
+ # Convert string keys back to symbols for consistent access in the app
48
+ convert_keys_to_symbols(result)
49
+ end
50
+ end
51
+
52
+ # Refresh system information by collecting new data
53
+ #
54
+ # @return [Hash] refreshed system information
55
+ def refresh_info
56
+ log_info "Refreshing system information"
57
+
58
+ info = Services::SystemInfo::SystemInfoCollector.call
59
+ save_info(info)
60
+
61
+ log_info "System information refreshed successfully"
62
+ # Return the info with symbol keys for consistent access
63
+ convert_keys_to_symbols(info)
64
+ rescue StandardError => e
65
+ log_error "Failed to refresh system information: #{e.message}"
66
+
67
+ # Return cached info if available, otherwise empty hash with error
68
+ cached_info = load_info
69
+ cached_info.empty? ? { error: e.message } : cached_info
70
+ end
71
+
72
+ # Get cached system information with TTL support
73
+ #
74
+ # @param max_age [Integer] maximum age in seconds (default: from config)
75
+ # @return [Hash] cached or refreshed system information
76
+ def cached_info(max_age: nil)
77
+ max_age ||= Dbwatcher.configuration.system_info_cache_duration
78
+
79
+ info = load_info
80
+
81
+ # If no cached info exists, collect new data
82
+ return refresh_info if info.empty?
83
+
84
+ # Check if cached info is expired
85
+ if info_expired?(info, max_age)
86
+ log_info "System information cache expired, refreshing"
87
+ return refresh_info
88
+ end
89
+
90
+ log_info "Using cached system information"
91
+ info
92
+ rescue StandardError => e
93
+ log_error "Failed to get cached system information: #{e.message}"
94
+ { error: e.message }
95
+ end
96
+
97
+ # Check if system information is available
98
+ #
99
+ # @return [Boolean] true if system information exists
100
+ def info_available?
101
+ !load_info.empty?
102
+ rescue StandardError => e
103
+ log_error "Failed to check info availability: #{e.message}"
104
+ false
105
+ end
106
+
107
+ # Get system information age in seconds
108
+ #
109
+ # @return [Integer, nil] age in seconds or nil if not available
110
+ def info_age
111
+ info = load_info
112
+ return nil if info.empty? || !info[:collected_at]
113
+
114
+ collected_at = info[:collected_at]
115
+ current_time - Time.parse(collected_at)
116
+ rescue StandardError => e
117
+ log_error "Failed to get info age: #{e.message}"
118
+ nil
119
+ end
120
+
121
+ # Clear cached system information
122
+ #
123
+ # @return [Boolean] true if successful
124
+ def clear_cache
125
+ log_info "Clearing system information cache"
126
+ safe_delete_file(@info_file)
127
+ end
128
+
129
+ # Get system information summary for dashboard
130
+ #
131
+ # @return [Hash] summary information
132
+ # rubocop:disable Metrics/MethodLength
133
+ def summary
134
+ info = cached_info
135
+ return {} if info.empty? || info[:error]
136
+
137
+ {
138
+ hostname: dig_with_indifferent_access(info, :machine, :hostname),
139
+ os: dig_with_indifferent_access(info, :machine, :os, :name),
140
+ ruby_version: dig_with_indifferent_access(info, :runtime, :ruby_version),
141
+ rails_version: dig_with_indifferent_access(info, :runtime, :rails_version),
142
+ database_adapter: dig_with_indifferent_access(info, :database, :adapter, :name),
143
+ memory_usage: dig_with_indifferent_access(info, :machine, :memory, :usage_percent),
144
+ cpu_load: dig_with_indifferent_access(info, :machine, :load_average, "1min") ||
145
+ dig_with_indifferent_access(info, :machine, :cpu, :load_average, "1min") ||
146
+ dig_with_indifferent_access(info, :machine, :load, :one_minute),
147
+ active_connections: dig_with_indifferent_access(info, :database, :active_connections) ||
148
+ dig_with_indifferent_access(info, :database, :connection_pool, :connections),
149
+ collected_at: info[:collected_at],
150
+ collection_duration: info[:collection_duration]
151
+ }
152
+ rescue StandardError => e
153
+ log_error "Failed to get system info summary: #{e.message}"
154
+ {}
155
+ end
156
+ # rubocop:enable Metrics/MethodLength
157
+
158
+ private
159
+
160
+ # Check if system information is expired
161
+ #
162
+ # @param info [Hash] system information data
163
+ # @param max_age [Integer] maximum age in seconds
164
+ # @return [Boolean] true if expired
165
+ def info_expired?(info, max_age)
166
+ collected_at = info[:collected_at]
167
+ return true unless collected_at
168
+
169
+ (current_time - Time.parse(collected_at)) > max_age
170
+ rescue StandardError => e
171
+ log_error "Failed to check info expiration: #{e.message}"
172
+ true # Assume expired on error
173
+ end
174
+
175
+ # Get current time, using Rails method if available
176
+ #
177
+ # @return [Time] current time
178
+ def current_time
179
+ defined?(Time.current) ? Time.current : Time.now
180
+ end
181
+
182
+ # Convert all hash keys to strings recursively
183
+ #
184
+ # @param hash [Hash] hash to convert
185
+ # @return [Hash] hash with string keys
186
+ def convert_keys_to_strings(hash)
187
+ return hash unless hash.is_a?(Hash)
188
+
189
+ hash.each_with_object({}) do |(key, value), result|
190
+ string_key = key.to_s
191
+ result[string_key] = if value.is_a?(Hash)
192
+ convert_keys_to_strings(value)
193
+ elsif value.is_a?(Array)
194
+ value.map { |v| v.is_a?(Hash) ? convert_keys_to_strings(v) : v }
195
+ else
196
+ value
197
+ end
198
+ end
199
+ end
200
+
201
+ # Convert all hash keys to symbols recursively
202
+ #
203
+ # @param hash [Hash] hash to convert
204
+ # @return [Hash] hash with symbol keys
205
+ def convert_keys_to_symbols(hash)
206
+ return hash unless hash.is_a?(Hash)
207
+
208
+ hash.each_with_object({}) do |(key, value), result|
209
+ symbol_key = key.to_s.to_sym
210
+ result[symbol_key] = if value.is_a?(Hash)
211
+ convert_keys_to_symbols(value)
212
+ elsif value.is_a?(Array)
213
+ value.map { |v| v.is_a?(Hash) ? convert_keys_to_symbols(v) : v }
214
+ else
215
+ value
216
+ end
217
+ end
218
+ end
219
+
220
+ # Safe access to nested hash values with indifferent access
221
+ #
222
+ # @param hash [Hash] hash to access
223
+ # @param keys [Array] keys to access
224
+ # @return [Object, nil] value or nil if not found
225
+ def dig_with_indifferent_access(hash, *keys)
226
+ return nil unless hash.is_a?(Hash)
227
+
228
+ current = hash
229
+ keys.each do |key|
230
+ key_sym = key.to_s.to_sym
231
+ key_str = key.to_s
232
+ return nil unless current.is_a?(Hash)
233
+
234
+ current = current[key_sym] || current[key_str]
235
+ return nil if current.nil?
236
+ end
237
+ current
238
+ end
239
+ end
240
+ # rubocop:enable Metrics/ClassLength
241
+ end
242
+ end
@@ -9,6 +9,7 @@ require_relative "storage/session_storage"
9
9
  require_relative "storage/query_storage"
10
10
  require_relative "storage/table_storage"
11
11
  require_relative "storage/session_query"
12
+ require_relative "storage/system_info_storage"
12
13
  require_relative "storage/api/base_api"
13
14
  require_relative "storage/api/query_api"
14
15
  require_relative "storage/api/table_api"
@@ -68,6 +69,16 @@ module Dbwatcher
68
69
  @tables ||= Api::TableAPI.new(table_storage)
69
70
  end
70
71
 
72
+ # Provides access to system information operations
73
+ #
74
+ # @return [SystemInfoStorage] system info storage instance
75
+ # @example
76
+ # Dbwatcher::Storage.system_info.cached_info
77
+ # Dbwatcher::Storage.system_info.refresh_info
78
+ def system_info
79
+ @system_info ||= SystemInfoStorage.new
80
+ end
81
+
71
82
  # Resets all cached storage instances (primarily for testing)
72
83
  #
73
84
  # This method clears all memoized storage instances, forcing them
@@ -80,6 +91,7 @@ module Dbwatcher
80
91
  @session_storage = nil
81
92
  @query_storage = nil
82
93
  @table_storage = nil
94
+ @system_info = nil
83
95
  @sessions = nil
84
96
  @queries = nil
85
97
  @tables = nil
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dbwatcher
4
- VERSION = "1.1.1"
4
+ VERSION = "1.1.2"
5
5
  end
data/lib/dbwatcher.rb CHANGED
@@ -13,12 +13,12 @@ require_relative "dbwatcher/logging"
13
13
 
14
14
  # Storage layer
15
15
  require_relative "dbwatcher/storage"
16
+ require_relative "dbwatcher/middleware"
16
17
 
17
18
  # Tracking and SQL monitoring
18
19
  require_relative "dbwatcher/tracker"
19
20
  require_relative "dbwatcher/sql_logger"
20
21
  require_relative "dbwatcher/model_extension"
21
- require_relative "dbwatcher/middleware"
22
22
 
23
23
  # Base services
24
24
  require_relative "dbwatcher/services/base_service"
@@ -28,6 +28,12 @@ require_relative "dbwatcher/services/table_statistics_collector"
28
28
  require_relative "dbwatcher/services/dashboard_data_aggregator"
29
29
  require_relative "dbwatcher/services/query_filter_processor"
30
30
 
31
+ # System info services
32
+ require_relative "dbwatcher/services/system_info/machine_info_collector"
33
+ require_relative "dbwatcher/services/system_info/database_info_collector"
34
+ require_relative "dbwatcher/services/system_info/runtime_info_collector"
35
+ require_relative "dbwatcher/services/system_info/system_info_collector"
36
+
31
37
  # General analyzers
32
38
  require_relative "dbwatcher/services/analyzers/session_data_processor"
33
39
  require_relative "dbwatcher/services/analyzers/table_summary_builder"
@@ -75,16 +81,24 @@ require_relative "dbwatcher/services/api/diagram_data_service"
75
81
  # Rails engine
76
82
  require_relative "dbwatcher/engine" if defined?(Rails)
77
83
 
84
+ # DBWatcher module
78
85
  module Dbwatcher
79
86
  class Error < StandardError; end
80
87
 
81
88
  class << self
82
89
  attr_writer :configuration
83
90
 
91
+ # Get configuration
92
+ #
93
+ # @return [Configuration] configuration
84
94
  def configuration
85
95
  @configuration ||= Configuration.new
86
96
  end
87
97
 
98
+ # Configure DBWatcher
99
+ #
100
+ # @yield [configuration] configuration
101
+ # @return [Configuration] configuration
88
102
  def configure
89
103
  yield(configuration)
90
104
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbwatcher
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Huy Nguyen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-08 00:00:00.000000000 Z
11
+ date: 2025-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -169,6 +169,7 @@ files:
169
169
  - app/assets/javascripts/dbwatcher/auto_init.js
170
170
  - app/assets/javascripts/dbwatcher/components/base.js
171
171
  - app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js
172
+ - app/assets/javascripts/dbwatcher/components/dashboard.js
172
173
  - app/assets/javascripts/dbwatcher/components/diagrams.js
173
174
  - app/assets/javascripts/dbwatcher/components/summary.js
174
175
  - app/assets/javascripts/dbwatcher/core/alpine_store.js
@@ -188,37 +189,36 @@ files:
188
189
  - app/assets/stylesheets/dbwatcher/components/_diagrams.scss
189
190
  - app/assets/stylesheets/dbwatcher/components/_forms.scss
190
191
  - app/assets/stylesheets/dbwatcher/components/_navigation.scss
192
+ - app/assets/stylesheets/dbwatcher/components/_tabulator.scss
191
193
  - app/assets/stylesheets/dbwatcher/core/_base.scss
192
194
  - app/assets/stylesheets/dbwatcher/core/_variables.scss
195
+ - app/assets/stylesheets/dbwatcher/vendor/_tabulator_overrides.scss
193
196
  - app/assets/stylesheets/dbwatcher/vendor/tabulator.min.css
194
197
  - app/controllers/dbwatcher/api/v1/sessions_controller.rb
198
+ - app/controllers/dbwatcher/api/v1/system_info_controller.rb
195
199
  - app/controllers/dbwatcher/base_controller.rb
200
+ - app/controllers/dbwatcher/dashboard/system_info_controller.rb
196
201
  - app/controllers/dbwatcher/dashboard_controller.rb
197
202
  - app/controllers/dbwatcher/queries_controller.rb
198
203
  - app/controllers/dbwatcher/sessions_controller.rb
199
204
  - app/controllers/dbwatcher/tables_controller.rb
200
205
  - app/helpers/dbwatcher/application_helper.rb
201
- - app/helpers/dbwatcher/component_helper.rb
202
206
  - app/helpers/dbwatcher/diagram_helper.rb
203
207
  - app/helpers/dbwatcher/formatting_helper.rb
204
208
  - app/helpers/dbwatcher/session_helper.rb
209
+ - app/views/dbwatcher/dashboard/_layout.html.erb
210
+ - app/views/dbwatcher/dashboard/_overview.html.erb
211
+ - app/views/dbwatcher/dashboard/_system_info.html.erb
212
+ - app/views/dbwatcher/dashboard/_system_info_content.html.erb
205
213
  - app/views/dbwatcher/dashboard/index.html.erb
206
214
  - app/views/dbwatcher/queries/index.html.erb
207
- - app/views/dbwatcher/sessions/_changes_tab.html.erb
208
- - app/views/dbwatcher/sessions/_diagrams_tab.html.erb
215
+ - app/views/dbwatcher/sessions/_changes.html.erb
216
+ - app/views/dbwatcher/sessions/_diagrams.html.erb
217
+ - app/views/dbwatcher/sessions/_layout.html.erb
209
218
  - app/views/dbwatcher/sessions/_session_header.html.erb
210
- - app/views/dbwatcher/sessions/_summary_tab.html.erb
211
- - app/views/dbwatcher/sessions/_tab_navigation.html.erb
212
- - app/views/dbwatcher/sessions/changes.html.erb
213
- - app/views/dbwatcher/sessions/components/changes/_filters.html.erb
214
- - app/views/dbwatcher/sessions/components/changes/_table_list.html.erb
215
- - app/views/dbwatcher/sessions/diagrams.html.erb
219
+ - app/views/dbwatcher/sessions/_summary.html.erb
216
220
  - app/views/dbwatcher/sessions/index.html.erb
217
- - app/views/dbwatcher/sessions/shared/_layout.html.erb
218
- - app/views/dbwatcher/sessions/shared/_navigation.html.erb
219
- - app/views/dbwatcher/sessions/shared/_session_header.html.erb
220
221
  - app/views/dbwatcher/sessions/show.html.erb
221
- - app/views/dbwatcher/sessions/summary.html.erb
222
222
  - app/views/dbwatcher/shared/_badge.html.erb
223
223
  - app/views/dbwatcher/shared/_data_table.html.erb
224
224
  - app/views/dbwatcher/shared/_header.html.erb
@@ -276,6 +276,10 @@ files:
276
276
  - lib/dbwatcher/services/mermaid_syntax/sanitizer.rb
277
277
  - lib/dbwatcher/services/mermaid_syntax_builder.rb
278
278
  - lib/dbwatcher/services/query_filter_processor.rb
279
+ - lib/dbwatcher/services/system_info/database_info_collector.rb
280
+ - lib/dbwatcher/services/system_info/machine_info_collector.rb
281
+ - lib/dbwatcher/services/system_info/runtime_info_collector.rb
282
+ - lib/dbwatcher/services/system_info/system_info_collector.rb
279
283
  - lib/dbwatcher/services/table_statistics_collector.rb
280
284
  - lib/dbwatcher/sql_logger.rb
281
285
  - lib/dbwatcher/storage.rb
@@ -300,6 +304,7 @@ files:
300
304
  - lib/dbwatcher/storage/session_operations.rb
301
305
  - lib/dbwatcher/storage/session_query.rb
302
306
  - lib/dbwatcher/storage/session_storage.rb
307
+ - lib/dbwatcher/storage/system_info_storage.rb
303
308
  - lib/dbwatcher/storage/table_storage.rb
304
309
  - lib/dbwatcher/tracker.rb
305
310
  - lib/dbwatcher/version.rb
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Dbwatcher
4
- module ComponentHelper
5
- # Removed as part of API-first migration (Story 6.8)
6
- # All data is now loaded directly from API endpoints
7
-
8
- # Removed as part of API-first migration (Story 6.8)
9
- # All data is now loaded directly from API endpoints
10
-
11
- # Removed as part of API-first migration (Story 6.8)
12
- # All data is now loaded directly from API endpoints
13
-
14
- # Generate data attributes for component binding
15
- def dbwatcher_component(component_name, config)
16
- {
17
- "data-dbwatcher" => component_name,
18
- "data-config" => config.to_json
19
- }
20
- end
21
-
22
- # Helper to render a DBWatcher component
23
- def render_dbwatcher_component(component_name, config, html_options = {})
24
- content_tag :div, dbwatcher_component(component_name, config).merge(html_options) do
25
- yield if block_given?
26
- end
27
- end
28
- end
29
- end