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.
- checksums.yaml +4 -4
- data/README.md +24 -2
- data/app/assets/config/dbwatcher_manifest.js +1 -0
- data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +196 -119
- data/app/assets/javascripts/dbwatcher/components/dashboard.js +325 -0
- data/app/assets/javascripts/dbwatcher/components/timeline.js +211 -0
- data/app/assets/javascripts/dbwatcher/dbwatcher.js +5 -0
- data/app/assets/stylesheets/dbwatcher/application.css +691 -41
- data/app/assets/stylesheets/dbwatcher/application.scss +5 -0
- data/app/assets/stylesheets/dbwatcher/components/_badges.scss +68 -23
- data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +83 -26
- data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +3 -3
- data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +9 -0
- data/app/assets/stylesheets/dbwatcher/components/_tabulator.scss +248 -0
- data/app/assets/stylesheets/dbwatcher/components/_timeline.scss +326 -0
- data/app/assets/stylesheets/dbwatcher/vendor/_tabulator_overrides.scss +37 -0
- data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +18 -4
- data/app/controllers/dbwatcher/api/v1/system_info_controller.rb +180 -0
- data/app/controllers/dbwatcher/dashboard/system_info_controller.rb +64 -0
- data/app/controllers/dbwatcher/dashboard_controller.rb +17 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +3 -19
- data/app/helpers/dbwatcher/application_helper.rb +43 -11
- data/app/helpers/dbwatcher/diagram_helper.rb +0 -88
- data/app/views/dbwatcher/dashboard/_layout.html.erb +27 -0
- data/app/views/dbwatcher/dashboard/_overview.html.erb +188 -0
- data/app/views/dbwatcher/dashboard/_system_info.html.erb +22 -0
- data/app/views/dbwatcher/dashboard/_system_info_content.html.erb +389 -0
- data/app/views/dbwatcher/dashboard/index.html.erb +8 -177
- data/app/views/dbwatcher/sessions/_layout.html.erb +26 -0
- data/app/views/dbwatcher/sessions/{_summary_tab.html.erb → _summary.html.erb} +1 -1
- data/app/views/dbwatcher/sessions/_tables.html.erb +170 -0
- data/app/views/dbwatcher/sessions/_timeline.html.erb +260 -0
- data/app/views/dbwatcher/sessions/index.html.erb +107 -87
- data/app/views/dbwatcher/sessions/show.html.erb +12 -4
- data/app/views/dbwatcher/tables/index.html.erb +32 -40
- data/app/views/layouts/dbwatcher/application.html.erb +101 -48
- data/config/routes.rb +25 -7
- data/lib/dbwatcher/configuration.rb +18 -1
- data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +102 -1
- data/lib/dbwatcher/services/api/{changes_data_service.rb → tables_data_service.rb} +6 -6
- data/lib/dbwatcher/services/base_service.rb +2 -0
- data/lib/dbwatcher/services/system_info/database_info_collector.rb +263 -0
- data/lib/dbwatcher/services/system_info/machine_info_collector.rb +387 -0
- data/lib/dbwatcher/services/system_info/runtime_info_collector.rb +328 -0
- data/lib/dbwatcher/services/system_info/system_info_collector.rb +114 -0
- data/lib/dbwatcher/services/timeline_data_service/enhancement_utilities.rb +100 -0
- data/lib/dbwatcher/services/timeline_data_service/entry_builder.rb +125 -0
- data/lib/dbwatcher/services/timeline_data_service/metadata_builder.rb +93 -0
- data/lib/dbwatcher/services/timeline_data_service.rb +130 -0
- data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +1 -1
- data/lib/dbwatcher/storage/concerns/error_handler.rb +6 -6
- data/lib/dbwatcher/storage/session.rb +5 -0
- data/lib/dbwatcher/storage/system_info_storage.rb +242 -0
- data/lib/dbwatcher/storage.rb +12 -0
- data/lib/dbwatcher/version.rb +1 -1
- data/lib/dbwatcher.rb +16 -2
- metadata +28 -16
- data/app/helpers/dbwatcher/component_helper.rb +0 -29
- data/app/views/dbwatcher/sessions/_changes_tab.html.erb +0 -265
- data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +0 -12
- data/app/views/dbwatcher/sessions/changes.html.erb +0 -21
- data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +0 -44
- data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +0 -96
- data/app/views/dbwatcher/sessions/diagrams.html.erb +0 -21
- data/app/views/dbwatcher/sessions/shared/_layout.html.erb +0 -8
- data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +0 -35
- data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +0 -25
- data/app/views/dbwatcher/sessions/summary.html.erb +0 -21
- /data/app/views/dbwatcher/sessions/{_diagrams_tab.html.erb → _diagrams.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
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
class TimelineDataService
|
6
|
+
# Module for enhancing timeline entries and utility methods
|
7
|
+
module EnhancementUtilities
|
8
|
+
private
|
9
|
+
|
10
|
+
# Enhance timeline entries with additional metadata
|
11
|
+
#
|
12
|
+
# @return [void]
|
13
|
+
def enhance_with_metadata
|
14
|
+
return if @timeline_entries.empty?
|
15
|
+
|
16
|
+
session_start_time = @timeline_entries.first[:raw_timestamp]
|
17
|
+
|
18
|
+
@timeline_entries.each_with_index do |entry, index|
|
19
|
+
entry[:relative_time] = calculate_relative_time(entry[:raw_timestamp], session_start_time)
|
20
|
+
entry[:duration_from_previous] = calculate_duration_from_previous(entry, index)
|
21
|
+
entry[:operation_group] = determine_operation_group(entry, index)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Calculate relative time from session start
|
26
|
+
#
|
27
|
+
# @param timestamp [Float] entry timestamp
|
28
|
+
# @param session_start [Float] session start timestamp
|
29
|
+
# @return [String] formatted relative time
|
30
|
+
def calculate_relative_time(timestamp, session_start)
|
31
|
+
seconds = timestamp - session_start
|
32
|
+
format_duration(seconds)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Calculate duration from previous operation
|
36
|
+
#
|
37
|
+
# @param entry [Hash] current entry
|
38
|
+
# @param index [Integer] entry index
|
39
|
+
# @return [Integer] duration in milliseconds
|
40
|
+
def calculate_duration_from_previous(entry, index)
|
41
|
+
return 0 if index.zero?
|
42
|
+
|
43
|
+
previous_entry = @timeline_entries[index - 1]
|
44
|
+
((entry[:raw_timestamp] - previous_entry[:raw_timestamp]) * 1000).round
|
45
|
+
end
|
46
|
+
|
47
|
+
# Determine operation group for related operations
|
48
|
+
#
|
49
|
+
# @param entry [Hash] current entry
|
50
|
+
# @param index [Integer] entry index
|
51
|
+
# @return [String] operation group identifier
|
52
|
+
def determine_operation_group(entry, index)
|
53
|
+
# Group operations on same table within 1 second
|
54
|
+
return "single_op" if index.zero?
|
55
|
+
|
56
|
+
previous_entry = @timeline_entries[index - 1]
|
57
|
+
time_diff = entry[:raw_timestamp] - previous_entry[:raw_timestamp]
|
58
|
+
|
59
|
+
if time_diff <= 1.0 && entry[:table_name] == previous_entry[:table_name]
|
60
|
+
"#{entry[:table_name]}_batch_#{index / 10}" # Group every 10 operations
|
61
|
+
else
|
62
|
+
"single_op"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Format duration in human-readable format
|
67
|
+
#
|
68
|
+
# @param seconds [Float] duration in seconds
|
69
|
+
# @return [String] formatted duration
|
70
|
+
def format_duration(seconds)
|
71
|
+
if seconds < 60
|
72
|
+
format("%<minutes>02d:%<seconds>02d", minutes: 0, seconds: seconds.to_i)
|
73
|
+
elsif seconds < 3600
|
74
|
+
minutes = seconds / 60
|
75
|
+
format("%<minutes>02d:%<seconds>02d", minutes: minutes.to_i, seconds: (seconds % 60).to_i)
|
76
|
+
else
|
77
|
+
hours = seconds / 3600
|
78
|
+
minutes = (seconds % 3600) / 60
|
79
|
+
format("%<hours>02d:%<minutes>02d:%<seconds>02d",
|
80
|
+
hours: hours.to_i, minutes: minutes.to_i, seconds: (seconds % 60).to_i)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Get model class for a table using the TableSummaryBuilder service
|
85
|
+
#
|
86
|
+
# @param table_name [String] database table name
|
87
|
+
# @return [String, nil] model class name or nil if not found
|
88
|
+
def get_model_class_for_table(table_name)
|
89
|
+
# Use cache to avoid repeated lookups
|
90
|
+
@model_class_cache ||= {}
|
91
|
+
return @model_class_cache[table_name] if @model_class_cache.key?(table_name)
|
92
|
+
|
93
|
+
# Delegate to TableSummaryBuilder for model class lookup
|
94
|
+
builder = Dbwatcher::Services::Analyzers::TableSummaryBuilder.new(@session)
|
95
|
+
@model_class_cache[table_name] = builder.send(:find_model_class, table_name)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
|
5
|
+
module Dbwatcher
|
6
|
+
module Services
|
7
|
+
class TimelineDataService
|
8
|
+
# Module for building timeline entries
|
9
|
+
module EntryBuilder
|
10
|
+
private
|
11
|
+
|
12
|
+
# Create a timeline entry from change data
|
13
|
+
#
|
14
|
+
# @param change [Hash] change data
|
15
|
+
# @param sequence [Integer] sequence number
|
16
|
+
# @return [Hash] timeline entry
|
17
|
+
def create_timeline_entry(change, sequence)
|
18
|
+
timestamp = parse_timestamp(change[:timestamp])
|
19
|
+
|
20
|
+
{
|
21
|
+
id: generate_entry_id(change, sequence),
|
22
|
+
timestamp: timestamp,
|
23
|
+
sequence: sequence,
|
24
|
+
table_name: change[:table_name],
|
25
|
+
operation: change[:operation],
|
26
|
+
record_id: extract_record_id(change),
|
27
|
+
changes: format_changes(change),
|
28
|
+
metadata: extract_metadata(change),
|
29
|
+
model_class: get_model_class_for_table(change[:table_name]),
|
30
|
+
raw_timestamp: timestamp.to_f
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
# Generate unique ID for timeline entry
|
35
|
+
#
|
36
|
+
# @param change [Hash] change data
|
37
|
+
# @param sequence [Integer] sequence number
|
38
|
+
# @return [String] unique entry ID
|
39
|
+
def generate_entry_id(change, sequence)
|
40
|
+
data = "#{change[:table_name]}_#{change[:operation]}_#{sequence}"
|
41
|
+
hash = Digest::SHA1.hexdigest(data)[0..7]
|
42
|
+
"#{@session.id}_entry_#{sequence}_#{hash}"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Parse timestamp from various formats
|
46
|
+
#
|
47
|
+
# @param timestamp [String, Time, Integer] timestamp value
|
48
|
+
# @return [Time] parsed timestamp
|
49
|
+
def parse_timestamp(timestamp)
|
50
|
+
case timestamp
|
51
|
+
when Time
|
52
|
+
timestamp
|
53
|
+
when String
|
54
|
+
Time.parse(timestamp)
|
55
|
+
when Integer, Float
|
56
|
+
Time.at(timestamp)
|
57
|
+
else
|
58
|
+
Time.current
|
59
|
+
end
|
60
|
+
rescue ArgumentError
|
61
|
+
Time.current
|
62
|
+
end
|
63
|
+
|
64
|
+
# Extract record ID from change data
|
65
|
+
#
|
66
|
+
# @param change [Hash] change data
|
67
|
+
# @return [String, nil] record ID if available
|
68
|
+
def extract_record_id(change)
|
69
|
+
change[:record_id] || change[:id] || change.dig(:changes, :id)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Format changes for timeline display
|
73
|
+
#
|
74
|
+
# @param change [Hash] change data
|
75
|
+
# @return [Hash] formatted changes
|
76
|
+
def format_changes(change)
|
77
|
+
raw_changes = change[:changes] || change[:data] || {}
|
78
|
+
return {} unless raw_changes.is_a?(Hash)
|
79
|
+
|
80
|
+
raw_changes.transform_values do |value|
|
81
|
+
case value
|
82
|
+
when Hash
|
83
|
+
value # Already formatted as { from: x, to: y }
|
84
|
+
else
|
85
|
+
{ to: value } # Simple value change
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Extract metadata from change data
|
91
|
+
#
|
92
|
+
# @param change [Hash] change data
|
93
|
+
# @return [Hash] metadata hash
|
94
|
+
def extract_metadata(change)
|
95
|
+
{
|
96
|
+
duration_ms: change[:duration_ms] || change[:duration],
|
97
|
+
affected_rows: change[:affected_rows] || change[:rows_affected] || 1,
|
98
|
+
query_fingerprint: change[:query_fingerprint] || change[:sql_fingerprint],
|
99
|
+
connection_id: change[:connection_id] || change[:connection],
|
100
|
+
query_type: determine_query_type(change[:operation])
|
101
|
+
}.compact
|
102
|
+
end
|
103
|
+
|
104
|
+
# Determine query type from operation
|
105
|
+
#
|
106
|
+
# @param operation [String] database operation
|
107
|
+
# @return [String] query type
|
108
|
+
def determine_query_type(operation)
|
109
|
+
case operation&.upcase
|
110
|
+
when "INSERT", "CREATE"
|
111
|
+
"write"
|
112
|
+
when "UPDATE", "MODIFY"
|
113
|
+
"update"
|
114
|
+
when "DELETE", "DROP"
|
115
|
+
"delete"
|
116
|
+
when "SELECT", "SHOW"
|
117
|
+
"read"
|
118
|
+
else
|
119
|
+
"unknown"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|