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,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Services
|
5
|
+
class TimelineDataService
|
6
|
+
# Module for building timeline metadata
|
7
|
+
module MetadataBuilder
|
8
|
+
private
|
9
|
+
|
10
|
+
# Build timeline metadata
|
11
|
+
#
|
12
|
+
# @return [Hash] timeline metadata
|
13
|
+
def build_timeline_metadata
|
14
|
+
return {} if @timeline_entries.empty?
|
15
|
+
|
16
|
+
{
|
17
|
+
total_operations: @timeline_entries.length,
|
18
|
+
time_range: calculate_time_range,
|
19
|
+
session_duration: calculate_session_duration,
|
20
|
+
tables_affected: extract_affected_tables,
|
21
|
+
operation_counts: count_operations_by_type,
|
22
|
+
peak_activity_periods: find_peak_activity_periods
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
# Calculate time range for the session
|
27
|
+
#
|
28
|
+
# @return [Hash] time range with start and end
|
29
|
+
def calculate_time_range
|
30
|
+
return {} if @timeline_entries.empty?
|
31
|
+
|
32
|
+
start_time = Time.at(@timeline_entries.first[:raw_timestamp])
|
33
|
+
end_time = Time.at(@timeline_entries.last[:raw_timestamp])
|
34
|
+
|
35
|
+
{
|
36
|
+
start: start_time.iso8601,
|
37
|
+
end: end_time.iso8601
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
# Calculate total session duration
|
42
|
+
#
|
43
|
+
# @return [String] formatted session duration
|
44
|
+
def calculate_session_duration
|
45
|
+
return "00:00" if @timeline_entries.length < 2
|
46
|
+
|
47
|
+
duration = @timeline_entries.last[:raw_timestamp] - @timeline_entries.first[:raw_timestamp]
|
48
|
+
format_duration(duration)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Extract list of affected tables
|
52
|
+
#
|
53
|
+
# @return [Array<String>] unique table names
|
54
|
+
def extract_affected_tables
|
55
|
+
@timeline_entries.map { |entry| entry[:table_name] }.uniq.sort
|
56
|
+
end
|
57
|
+
|
58
|
+
# Count operations by type
|
59
|
+
#
|
60
|
+
# @return [Hash] operation counts
|
61
|
+
def count_operations_by_type
|
62
|
+
@timeline_entries.group_by { |entry| entry[:operation] }
|
63
|
+
.transform_values(&:count)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Find peak activity periods
|
67
|
+
#
|
68
|
+
# @return [Array<Hash>] peak activity periods
|
69
|
+
def find_peak_activity_periods
|
70
|
+
return [] if @timeline_entries.length < 10
|
71
|
+
|
72
|
+
# Group operations by 1-minute windows
|
73
|
+
windows = @timeline_entries.group_by do |entry|
|
74
|
+
timestamp = Time.at(entry[:raw_timestamp])
|
75
|
+
Time.new(timestamp.year, timestamp.month, timestamp.day, timestamp.hour, timestamp.min, 0)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Find windows with more than average activity
|
79
|
+
average_ops = @timeline_entries.length / windows.length.to_f
|
80
|
+
|
81
|
+
windows.select { |_, ops| ops.length > average_ops * 1.5 }
|
82
|
+
.map do |window_start, ops|
|
83
|
+
{
|
84
|
+
start: window_start.iso8601,
|
85
|
+
end: (window_start + 1.minute).iso8601,
|
86
|
+
operations_count: ops.length
|
87
|
+
}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "timeline_data_service/metadata_builder"
|
4
|
+
require_relative "timeline_data_service/entry_builder"
|
5
|
+
require_relative "timeline_data_service/enhancement_utilities"
|
6
|
+
|
7
|
+
module Dbwatcher
|
8
|
+
module Services
|
9
|
+
# Timeline Data Service for processing session data into chronological timeline format
|
10
|
+
#
|
11
|
+
# This service transforms session changes into a chronologically ordered timeline
|
12
|
+
# with enhanced metadata for visualization and filtering.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# service = TimelineDataService.new(session)
|
16
|
+
# result = service.call
|
17
|
+
# timeline = result[:timeline]
|
18
|
+
# metadata = result[:metadata]
|
19
|
+
class TimelineDataService
|
20
|
+
include MetadataBuilder
|
21
|
+
include EntryBuilder
|
22
|
+
include EnhancementUtilities
|
23
|
+
# Initialize the timeline data service
|
24
|
+
#
|
25
|
+
# @param session [Session] session object containing changes data
|
26
|
+
def initialize(session)
|
27
|
+
@session = session
|
28
|
+
@timeline_entries = []
|
29
|
+
@start_time = Time.current
|
30
|
+
end
|
31
|
+
|
32
|
+
# Process session data into timeline format
|
33
|
+
#
|
34
|
+
# @return [Hash] processed timeline data with metadata
|
35
|
+
def call
|
36
|
+
Rails.logger.info("Processing timeline data for session #{@session.id}")
|
37
|
+
|
38
|
+
validate_session_data
|
39
|
+
build_timeline_entries
|
40
|
+
sort_chronologically
|
41
|
+
enhance_with_metadata
|
42
|
+
result = build_result
|
43
|
+
|
44
|
+
Rails.logger.info(
|
45
|
+
"Timeline processing completed for session #{@session.id} (#{@timeline_entries.length} entries)"
|
46
|
+
)
|
47
|
+
|
48
|
+
result
|
49
|
+
rescue StandardError => e
|
50
|
+
Rails.logger.error("Timeline processing failed for session #{@session.id}: #{e.message}")
|
51
|
+
build_error_result(e)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
# Validate session data before processing
|
57
|
+
#
|
58
|
+
# @raise [ArgumentError] if session data is invalid
|
59
|
+
def validate_session_data
|
60
|
+
raise ArgumentError, "Session is required" unless @session
|
61
|
+
raise ArgumentError, "Session ID is required" unless @session.id
|
62
|
+
raise ArgumentError, "Session changes are required" unless @session.changes
|
63
|
+
end
|
64
|
+
|
65
|
+
# Build timeline entries from session changes
|
66
|
+
#
|
67
|
+
# @return [void]
|
68
|
+
def build_timeline_entries
|
69
|
+
@session.changes.each_with_index do |change, index|
|
70
|
+
next unless valid_change?(change)
|
71
|
+
|
72
|
+
@timeline_entries << create_timeline_entry(change, index)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Check if a change is valid for timeline processing
|
77
|
+
#
|
78
|
+
# @param change [Hash] change data
|
79
|
+
# @return [Boolean] true if change is valid
|
80
|
+
def valid_change?(change)
|
81
|
+
change.is_a?(Hash) &&
|
82
|
+
change[:table_name] &&
|
83
|
+
change[:operation] &&
|
84
|
+
change[:timestamp]
|
85
|
+
end
|
86
|
+
|
87
|
+
# Sort timeline entries chronologically
|
88
|
+
#
|
89
|
+
# @return [void]
|
90
|
+
def sort_chronologically
|
91
|
+
@timeline_entries.sort_by! { |entry| entry[:raw_timestamp] }
|
92
|
+
end
|
93
|
+
|
94
|
+
# Build final result hash
|
95
|
+
#
|
96
|
+
# @return [Hash] complete timeline result
|
97
|
+
def build_result
|
98
|
+
{
|
99
|
+
timeline: @timeline_entries,
|
100
|
+
metadata: build_timeline_metadata,
|
101
|
+
summary: build_timeline_summary,
|
102
|
+
errors: []
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
# Build timeline summary
|
107
|
+
#
|
108
|
+
# @return [Hash] timeline summary
|
109
|
+
def build_timeline_summary
|
110
|
+
{
|
111
|
+
total_entries: @timeline_entries.length,
|
112
|
+
processing_time: (Time.current - @start_time).round(3)
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
# Build error result
|
117
|
+
#
|
118
|
+
# @param error [StandardError] error that occurred
|
119
|
+
# @return [Hash] error result
|
120
|
+
def build_error_result(error)
|
121
|
+
{
|
122
|
+
timeline: [],
|
123
|
+
metadata: {},
|
124
|
+
summary: { error: error.message },
|
125
|
+
errors: [{ type: "processing_error", message: error.message }]
|
126
|
+
}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -24,7 +24,7 @@ module Dbwatcher
|
|
24
24
|
# @return [Hash] tables summary hash
|
25
25
|
def build_tables_summary(session)
|
26
26
|
# Delegate to new service while maintaining interface compatibility
|
27
|
-
Dbwatcher::Services::Analyzers::TableSummaryBuilder.
|
27
|
+
Dbwatcher::Services::Analyzers::TableSummaryBuilder.new(session).call
|
28
28
|
end
|
29
29
|
|
30
30
|
# Process all changes in a session (legacy method for backward compatibility)
|
@@ -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
|
-
|
31
|
+
log_error_with_exception("JSON parsing failed in #{operation_name}", e)
|
32
32
|
default_value
|
33
33
|
rescue Errno::ENOENT => e
|
34
|
-
|
34
|
+
log_error_with_exception("File not found in #{operation_name}", e)
|
35
35
|
default_value
|
36
36
|
rescue Errno::EACCES => e
|
37
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
@@ -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
|
data/lib/dbwatcher/storage.rb
CHANGED
@@ -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
|
data/lib/dbwatcher/version.rb
CHANGED
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"
|
@@ -68,23 +74,31 @@ require_relative "dbwatcher/services/diagram_system"
|
|
68
74
|
|
69
75
|
# API services
|
70
76
|
require_relative "dbwatcher/services/api/base_api_service"
|
71
|
-
require_relative "dbwatcher/services/api/
|
77
|
+
require_relative "dbwatcher/services/api/tables_data_service"
|
72
78
|
require_relative "dbwatcher/services/api/summary_data_service"
|
73
79
|
require_relative "dbwatcher/services/api/diagram_data_service"
|
74
80
|
|
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
|