dbwatcher 0.1.5 → 1.0.0
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 +2 -2
- data/app/controllers/dbwatcher/base_controller.rb +95 -0
- data/app/controllers/dbwatcher/dashboard_controller.rb +12 -0
- data/app/controllers/dbwatcher/queries_controller.rb +24 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +15 -20
- data/app/controllers/dbwatcher/tables_controller.rb +38 -0
- data/app/helpers/dbwatcher/application_helper.rb +103 -0
- data/app/helpers/dbwatcher/formatting_helper.rb +108 -0
- data/app/helpers/dbwatcher/session_helper.rb +27 -0
- data/app/views/dbwatcher/dashboard/index.html.erb +177 -0
- data/app/views/dbwatcher/queries/index.html.erb +240 -0
- data/app/views/dbwatcher/sessions/index.html.erb +120 -27
- data/app/views/dbwatcher/sessions/show.html.erb +326 -129
- data/app/views/dbwatcher/shared/_badge.html.erb +4 -0
- data/app/views/dbwatcher/shared/_data_table.html.erb +20 -0
- data/app/views/dbwatcher/shared/_header.html.erb +7 -0
- data/app/views/dbwatcher/shared/_page_layout.html.erb +20 -0
- data/app/views/dbwatcher/shared/_section_panel.html.erb +9 -0
- data/app/views/dbwatcher/shared/_stats_card.html.erb +11 -0
- data/app/views/dbwatcher/shared/_tab_bar.html.erb +6 -0
- data/app/views/dbwatcher/tables/changes.html.erb +225 -0
- data/app/views/dbwatcher/tables/index.html.erb +123 -0
- data/app/views/dbwatcher/tables/show.html.erb +86 -0
- data/app/views/layouts/dbwatcher/application.html.erb +375 -26
- data/config/routes.rb +17 -3
- data/lib/dbwatcher/configuration.rb +9 -1
- data/lib/dbwatcher/engine.rb +12 -7
- data/lib/dbwatcher/logging.rb +72 -0
- data/lib/dbwatcher/services/dashboard_data_aggregator.rb +121 -0
- data/lib/dbwatcher/services/query_filter_processor.rb +114 -0
- data/lib/dbwatcher/services/table_statistics_collector.rb +119 -0
- data/lib/dbwatcher/sql_logger.rb +107 -0
- data/lib/dbwatcher/storage/api/base_api.rb +134 -0
- data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +172 -0
- data/lib/dbwatcher/storage/api/query_api.rb +95 -0
- data/lib/dbwatcher/storage/api/session_api.rb +134 -0
- data/lib/dbwatcher/storage/api/table_api.rb +86 -0
- data/lib/dbwatcher/storage/base_storage.rb +113 -0
- data/lib/dbwatcher/storage/change_processor.rb +65 -0
- data/lib/dbwatcher/storage/concerns/data_normalizer.rb +134 -0
- data/lib/dbwatcher/storage/concerns/error_handler.rb +75 -0
- data/lib/dbwatcher/storage/concerns/timestampable.rb +74 -0
- data/lib/dbwatcher/storage/concerns/validatable.rb +117 -0
- data/lib/dbwatcher/storage/date_helper.rb +21 -0
- data/lib/dbwatcher/storage/errors.rb +86 -0
- data/lib/dbwatcher/storage/file_manager.rb +122 -0
- data/lib/dbwatcher/storage/null_session.rb +39 -0
- data/lib/dbwatcher/storage/query_storage.rb +338 -0
- data/lib/dbwatcher/storage/query_validator.rb +24 -0
- data/lib/dbwatcher/storage/session.rb +58 -0
- data/lib/dbwatcher/storage/session_operations.rb +37 -0
- data/lib/dbwatcher/storage/session_query.rb +71 -0
- data/lib/dbwatcher/storage/session_storage.rb +322 -0
- data/lib/dbwatcher/storage/table_storage.rb +237 -0
- data/lib/dbwatcher/storage.rb +112 -85
- data/lib/dbwatcher/tracker.rb +4 -55
- data/lib/dbwatcher/version.rb +1 -1
- data/lib/dbwatcher.rb +12 -2
- metadata +47 -1
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_api"
|
4
|
+
|
5
|
+
module Dbwatcher
|
6
|
+
module Storage
|
7
|
+
module Api
|
8
|
+
class QueryAPI < BaseAPI
|
9
|
+
# Filter queries by date
|
10
|
+
#
|
11
|
+
# @param date [Date, String] date to filter by
|
12
|
+
# @return [QueryAPI] self for method chaining
|
13
|
+
def for_date(date)
|
14
|
+
@date = date.is_a?(String) ? date : date.strftime("%Y-%m-%d")
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
# Filter to slow queries only
|
19
|
+
#
|
20
|
+
# @param threshold [Integer] duration threshold in milliseconds
|
21
|
+
# @return [QueryAPI] self for method chaining
|
22
|
+
def slow_only(threshold: 100)
|
23
|
+
filters[:slow_threshold] = threshold
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
# Filter queries by table name
|
28
|
+
#
|
29
|
+
# @param table_name [String] table name to filter by
|
30
|
+
# @return [QueryAPI] self for method chaining
|
31
|
+
def by_table(table_name)
|
32
|
+
filters[:table_name] = table_name
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
# Filter queries between dates
|
37
|
+
#
|
38
|
+
# @param start_date [Date, String] start date
|
39
|
+
# @param end_date [Date, String] end date
|
40
|
+
# @return [QueryAPI] self for method chaining
|
41
|
+
def between(start_date, end_date)
|
42
|
+
start_str = start_date.is_a?(String) ? start_date : start_date.strftime("%Y-%m-%d")
|
43
|
+
end_str = end_date.is_a?(String) ? end_date : end_date.strftime("%Y-%m-%d")
|
44
|
+
@date_range = Date.parse(start_str)..Date.parse(end_str)
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
# Get all filtered queries
|
49
|
+
#
|
50
|
+
# @return [Array<Hash>] filtered queries
|
51
|
+
def all
|
52
|
+
queries = fetch_queries
|
53
|
+
apply_filters(queries)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def fetch_queries
|
59
|
+
if @date_range
|
60
|
+
@date_range.map { |date| storage.load_for_date(date.strftime("%Y-%m-%d")) }.flatten
|
61
|
+
elsif @date
|
62
|
+
storage.load_for_date(@date)
|
63
|
+
else
|
64
|
+
recent_queries
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def apply_filters(queries)
|
69
|
+
result = queries
|
70
|
+
|
71
|
+
# Apply slow threshold filter using symbols only
|
72
|
+
if filters[:slow_threshold]
|
73
|
+
result = result.select do |q|
|
74
|
+
duration = safe_extract(q, :duration)
|
75
|
+
duration && duration > filters[:slow_threshold]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Apply table name filter using symbols only
|
80
|
+
result = result.select { |q| safe_extract(q, :table_name) == filters[:table_name] } if filters[:table_name]
|
81
|
+
|
82
|
+
# Apply common filters
|
83
|
+
apply_common_filters(result)
|
84
|
+
end
|
85
|
+
|
86
|
+
def recent_queries
|
87
|
+
(0..6).map do |days_ago|
|
88
|
+
date = (Date.today - days_ago).strftime("%Y-%m-%d")
|
89
|
+
storage.load_for_date(date)
|
90
|
+
end.flatten
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "time"
|
4
|
+
require_relative "base_api"
|
5
|
+
require_relative "concerns/table_analyzer"
|
6
|
+
|
7
|
+
module Dbwatcher
|
8
|
+
module Storage
|
9
|
+
module Api
|
10
|
+
class SessionAPI < BaseAPI
|
11
|
+
# Include table analysis capabilities for API layer
|
12
|
+
include Api::Concerns::TableAnalyzer
|
13
|
+
|
14
|
+
# Find a specific session by ID
|
15
|
+
#
|
16
|
+
# @param id [String] the session ID
|
17
|
+
# @return [Session, nil] the session object or nil if not found
|
18
|
+
def find(id)
|
19
|
+
storage.load(id)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Get all sessions
|
23
|
+
#
|
24
|
+
# @return [Array<Hash>] array of session data
|
25
|
+
def all
|
26
|
+
apply_filters(storage.all)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Filter to recent sessions
|
30
|
+
#
|
31
|
+
# @param days [Integer] number of days back to include
|
32
|
+
# @return [SessionAPI] self for method chaining
|
33
|
+
def recent(days: 7)
|
34
|
+
cutoff = Time.now - (days * 24 * 60 * 60)
|
35
|
+
where(started_after: cutoff)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Filter sessions that have changes
|
39
|
+
#
|
40
|
+
# @return [SessionAPI] self for method chaining
|
41
|
+
def with_changes
|
42
|
+
filters[:has_changes] = true
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
# Filter sessions by status
|
47
|
+
#
|
48
|
+
# @param status [String, Symbol] session status (e.g., :active, :completed)
|
49
|
+
# @return [SessionAPI] self for method chaining
|
50
|
+
def by_status(status)
|
51
|
+
filters[:status] = status.to_s
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
# Filter sessions by name pattern
|
56
|
+
#
|
57
|
+
# @param pattern [String] name pattern to match
|
58
|
+
# @return [SessionAPI] self for method chaining
|
59
|
+
def by_name(pattern)
|
60
|
+
filters[:name_pattern] = pattern
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
# Get sessions with table analysis
|
65
|
+
#
|
66
|
+
# @return [Array<Hash>] sessions with analyzed table data
|
67
|
+
def with_table_analysis
|
68
|
+
all.map do |session_info|
|
69
|
+
session = find(safe_extract(session_info, :id))
|
70
|
+
next session_info unless session
|
71
|
+
|
72
|
+
session_info.merge(
|
73
|
+
tables_summary: build_tables_summary(session)
|
74
|
+
)
|
75
|
+
end.compact
|
76
|
+
end
|
77
|
+
|
78
|
+
# Get the most active sessions (by change count)
|
79
|
+
#
|
80
|
+
# @param limit [Integer] maximum number of sessions to return
|
81
|
+
# @return [Array<Hash>] sessions ordered by activity
|
82
|
+
def most_active(limit: 10)
|
83
|
+
sessions_with_counts = all.map do |session_info|
|
84
|
+
session = find(safe_extract(session_info, :id))
|
85
|
+
change_count = session ? session.changes.length : 0
|
86
|
+
session_info.merge(change_count: change_count)
|
87
|
+
end
|
88
|
+
|
89
|
+
sessions_with_counts
|
90
|
+
.sort_by { |s| -s[:change_count] }
|
91
|
+
.first(limit)
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def apply_filters(sessions)
|
97
|
+
sessions
|
98
|
+
.then { |s| filter_by_start_time(s) }
|
99
|
+
.then { |s| filter_by_status(s) }
|
100
|
+
.then { |s| filter_by_name_pattern(s) }
|
101
|
+
.then { |s| filter_by_changes(s) }
|
102
|
+
.then { |s| apply_common_filters(s) }
|
103
|
+
end
|
104
|
+
|
105
|
+
def filter_by_start_time(sessions)
|
106
|
+
return sessions unless filters[:started_after]
|
107
|
+
|
108
|
+
apply_time_filter(sessions, :started_at)
|
109
|
+
end
|
110
|
+
|
111
|
+
def filter_by_status(sessions)
|
112
|
+
return sessions unless filters[:status]
|
113
|
+
|
114
|
+
sessions.select { |s| safe_extract(s, :status) == filters[:status] }
|
115
|
+
end
|
116
|
+
|
117
|
+
def filter_by_name_pattern(sessions)
|
118
|
+
return sessions unless filters[:name_pattern]
|
119
|
+
|
120
|
+
apply_pattern_filter(sessions, %i[name id], filters[:name_pattern])
|
121
|
+
end
|
122
|
+
|
123
|
+
def filter_by_changes(sessions)
|
124
|
+
return sessions unless filters[:has_changes]
|
125
|
+
|
126
|
+
sessions.select do |s|
|
127
|
+
session = find(safe_extract(s, :id))
|
128
|
+
session&.changes&.any?
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_api"
|
4
|
+
|
5
|
+
module Dbwatcher
|
6
|
+
module Storage
|
7
|
+
module Api
|
8
|
+
class TableAPI < BaseAPI
|
9
|
+
# Filter changes for specific table
|
10
|
+
#
|
11
|
+
# @param table_name [String] table name
|
12
|
+
# @return [TableAPI] self for method chaining
|
13
|
+
def changes_for(table_name)
|
14
|
+
@table_name = table_name
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
# Filter to recent changes
|
19
|
+
#
|
20
|
+
# @param days [Integer] number of days back
|
21
|
+
# @return [TableAPI] self for method chaining
|
22
|
+
def recent(days: 7)
|
23
|
+
filters[:recent_days] = days
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
# Filter by operation type
|
28
|
+
#
|
29
|
+
# @param operation [String, Symbol] operation type
|
30
|
+
# @return [TableAPI] self for method chaining
|
31
|
+
def by_operation(operation)
|
32
|
+
filters[:operation] = normalize_operation(operation)
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
# Get all filtered changes
|
37
|
+
#
|
38
|
+
# @return [Array<Hash>] filtered changes
|
39
|
+
def all
|
40
|
+
return [] unless @table_name
|
41
|
+
|
42
|
+
changes = storage.load_changes(@table_name)
|
43
|
+
apply_filters(changes)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get most active tables
|
47
|
+
#
|
48
|
+
# @param limit [Integer] maximum number of tables
|
49
|
+
# @return [Array<Hash>] most active tables
|
50
|
+
def most_active(limit: 10)
|
51
|
+
# This would require aggregating across all tables
|
52
|
+
# For now, return empty array - can be implemented later
|
53
|
+
# TODO: Implement most active tables functionality with limit parameter
|
54
|
+
_ = limit # Acknowledge the parameter for future use
|
55
|
+
[]
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def apply_filters(changes)
|
61
|
+
result = changes
|
62
|
+
|
63
|
+
# Apply recent filter using symbols only
|
64
|
+
if filters[:recent_days]
|
65
|
+
cutoff = Time.now - (filters[:recent_days] * 24 * 60 * 60)
|
66
|
+
result = result.select do |change|
|
67
|
+
timestamp = normalize_timestamp(safe_extract(change, :timestamp))
|
68
|
+
timestamp >= cutoff
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Apply operation filter using symbols only
|
73
|
+
if filters[:operation]
|
74
|
+
result = result.select do |change|
|
75
|
+
operation = normalize_operation(safe_extract(change, :operation))
|
76
|
+
operation == filters[:operation]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Apply common filters
|
81
|
+
apply_common_filters(result)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "file_manager"
|
4
|
+
require_relative "errors"
|
5
|
+
require_relative "concerns/error_handler"
|
6
|
+
require_relative "concerns/timestampable"
|
7
|
+
require_relative "concerns/validatable"
|
8
|
+
|
9
|
+
module Dbwatcher
|
10
|
+
module Storage
|
11
|
+
# Base class for all storage implementations
|
12
|
+
#
|
13
|
+
# Provides common functionality for storage classes including
|
14
|
+
# file management, error handling, validation, and timestamping.
|
15
|
+
# Follows the Ruby style guide patterns for class organization.
|
16
|
+
#
|
17
|
+
# @abstract Subclass and implement specific storage logic
|
18
|
+
# @example
|
19
|
+
# class MyStorage < BaseStorage
|
20
|
+
# include Concerns::Validatable
|
21
|
+
#
|
22
|
+
# validates_presence_of :id, :name
|
23
|
+
#
|
24
|
+
# def save(data)
|
25
|
+
# validate_presence!(data)
|
26
|
+
# safe_write_json(file_path, data)
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
class BaseStorage
|
30
|
+
# Include shared concerns
|
31
|
+
include Concerns::ErrorHandler
|
32
|
+
include Concerns::Timestampable
|
33
|
+
|
34
|
+
# Configuration constants
|
35
|
+
DEFAULT_PERMISSIONS = 0o755
|
36
|
+
JSON_FILE_EXTENSION = ".json"
|
37
|
+
|
38
|
+
# @return [String] the configured storage path
|
39
|
+
attr_reader :storage_path
|
40
|
+
|
41
|
+
# @return [FileManager] the file manager instance
|
42
|
+
attr_reader :file_manager
|
43
|
+
|
44
|
+
# Initializes the base storage with configured path and file manager
|
45
|
+
#
|
46
|
+
# Sets up the storage path from configuration, creates a file manager
|
47
|
+
# instance, and initializes timestamps.
|
48
|
+
#
|
49
|
+
# @param storage_path [String, nil] custom storage path (optional)
|
50
|
+
def initialize(storage_path = nil)
|
51
|
+
@storage_path = storage_path || Dbwatcher.configuration.storage_path
|
52
|
+
@file_manager = FileManager.new(@storage_path)
|
53
|
+
initialize_timestamps
|
54
|
+
ensure_storage_directory
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
# Safely writes JSON data to a file
|
60
|
+
#
|
61
|
+
# @param file_path [String] the path to write to
|
62
|
+
# @param data [Object] the data to serialize as JSON
|
63
|
+
# @return [Boolean] true if successful, false otherwise
|
64
|
+
def safe_write_json(file_path, data)
|
65
|
+
safe_operation("write JSON to #{file_path}") do
|
66
|
+
file_manager.write_json(file_path, data)
|
67
|
+
touch_updated_at
|
68
|
+
true
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Safely reads JSON data from a file
|
73
|
+
#
|
74
|
+
# @param file_path [String] the path to read from
|
75
|
+
# @param default [Object] default value if file doesn't exist or is invalid
|
76
|
+
# @return [Object] the parsed JSON data or default value
|
77
|
+
def safe_read_json(file_path, default = [])
|
78
|
+
safe_operation("read JSON from #{file_path}", default) do
|
79
|
+
file_manager.read_json(file_path)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Removes a file safely
|
84
|
+
#
|
85
|
+
# @param file_path [String] the path to the file to remove
|
86
|
+
# @return [Boolean] true if file was removed, false if it didn't exist
|
87
|
+
def safe_delete_file(file_path)
|
88
|
+
safe_operation("delete file #{file_path}") do
|
89
|
+
file_manager.delete_file(file_path)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Removes a directory safely
|
94
|
+
#
|
95
|
+
# @params directory_path [String] the path to the directory to remove
|
96
|
+
# @return [Boolean] true if directory was removed, false if it didn't exist
|
97
|
+
def safe_delete_directory(directory_path)
|
98
|
+
safe_operation("delete directory #{directory_path}") do
|
99
|
+
file_manager.delete_directory(directory_path)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
# Ensures the storage directory exists with proper permissions
|
106
|
+
#
|
107
|
+
# @return [void]
|
108
|
+
def ensure_storage_directory
|
109
|
+
file_manager.ensure_directory(storage_path)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Storage
|
5
|
+
class ChangeProcessor
|
6
|
+
include Concerns::DataNormalizer
|
7
|
+
|
8
|
+
def initialize(session_storage)
|
9
|
+
@session_storage = session_storage
|
10
|
+
@session_cache = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def process_table_changes(table_name)
|
14
|
+
all_changes = collect_all_changes(table_name)
|
15
|
+
sort_changes_by_timestamp(all_changes)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def collect_all_changes(table_name)
|
21
|
+
changes = []
|
22
|
+
|
23
|
+
@session_storage.all.each do |session_info|
|
24
|
+
session_changes = extract_table_changes_from_session(session_info[:id], table_name)
|
25
|
+
changes.concat(session_changes)
|
26
|
+
end
|
27
|
+
|
28
|
+
changes
|
29
|
+
end
|
30
|
+
|
31
|
+
def extract_table_changes_from_session(session_id, table_name)
|
32
|
+
session = load_session_with_cache(session_id)
|
33
|
+
return [] unless session.respond_to?(:changes) && session.changes
|
34
|
+
|
35
|
+
session.changes
|
36
|
+
.select { |change| matches_table?(change, table_name) }
|
37
|
+
.map { |change| enrich_change(change, session) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def load_session_with_cache(session_id)
|
41
|
+
@session_cache[session_id] ||= @session_storage.load(session_id)
|
42
|
+
end
|
43
|
+
|
44
|
+
def matches_table?(change, table_name)
|
45
|
+
extract_value(change, :table_name) == table_name
|
46
|
+
end
|
47
|
+
|
48
|
+
def enrich_change(change, session)
|
49
|
+
normalized_change = normalize_change(change)
|
50
|
+
add_session_context(normalized_change, session)
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_session_context(change, session)
|
54
|
+
change.merge(
|
55
|
+
session_id: session.id,
|
56
|
+
session_name: session.name
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
def sort_changes_by_timestamp(changes)
|
61
|
+
changes.sort_by { |change| normalize_timestamp(extract_value(change, "timestamp")) }.reverse
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Storage
|
5
|
+
module Concerns
|
6
|
+
# Provides consistent data normalization capabilities across storage classes
|
7
|
+
#
|
8
|
+
# This concern standardizes how different data types are normalized to ensure
|
9
|
+
# consistent symbol-key usage and proper data formatting throughout the storage layer.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# class MyStorage < BaseStorage
|
13
|
+
# include Concerns::DataNormalizer
|
14
|
+
#
|
15
|
+
# def save(data)
|
16
|
+
# normalized_data = normalize_session_data(data)
|
17
|
+
# # ... save logic
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
module DataNormalizer
|
21
|
+
# Normalizes session input to hash with consistent symbol keys
|
22
|
+
#
|
23
|
+
# @param session [Hash, Object] session data to normalize
|
24
|
+
# @return [Hash] normalized session hash with symbol keys
|
25
|
+
def normalize_session_data(session)
|
26
|
+
case session
|
27
|
+
when Hash
|
28
|
+
normalize_hash_keys(session)
|
29
|
+
when ->(s) { s.respond_to?(:to_h) }
|
30
|
+
normalize_hash_keys(session.to_h)
|
31
|
+
else
|
32
|
+
extract_object_attributes(session)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Normalizes hash keys to symbols (Rails-compatible)
|
37
|
+
#
|
38
|
+
# @param hash [Hash] hash to normalize
|
39
|
+
# @return [Hash] hash with symbolized keys
|
40
|
+
def normalize_hash_keys(hash)
|
41
|
+
return hash unless hash.is_a?(Hash)
|
42
|
+
|
43
|
+
if hash.respond_to?(:with_indifferent_access)
|
44
|
+
hash.with_indifferent_access.symbolize_keys
|
45
|
+
else
|
46
|
+
hash.transform_keys { |key| key.to_s.to_sym }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Normalize change data to use consistent symbol keys
|
51
|
+
#
|
52
|
+
# @param change [Hash] change data to normalize
|
53
|
+
# @return [Hash] normalized change hash with symbol keys
|
54
|
+
def normalize_change(change)
|
55
|
+
return change unless change.is_a?(Hash)
|
56
|
+
|
57
|
+
normalize_hash_keys(change)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Extract value by trying both string and symbol keys
|
61
|
+
#
|
62
|
+
# @param hash [Hash] hash to extract from
|
63
|
+
# @param key [String, Symbol] key to extract
|
64
|
+
# @return [Object] extracted value or nil
|
65
|
+
def extract_value(hash, key)
|
66
|
+
return nil unless hash.is_a?(Hash)
|
67
|
+
|
68
|
+
hash[key.to_sym] || hash[key.to_s]
|
69
|
+
end
|
70
|
+
|
71
|
+
# Normalize timestamp to consistent format
|
72
|
+
#
|
73
|
+
# @param timestamp [String, Time, Numeric] timestamp to normalize
|
74
|
+
# @return [Time] normalized timestamp
|
75
|
+
def normalize_timestamp(timestamp)
|
76
|
+
return Time.at(0) unless timestamp
|
77
|
+
|
78
|
+
case timestamp
|
79
|
+
when String
|
80
|
+
Time.parse(timestamp)
|
81
|
+
when Time
|
82
|
+
timestamp
|
83
|
+
when Numeric
|
84
|
+
Time.at(timestamp)
|
85
|
+
else
|
86
|
+
Time.at(0)
|
87
|
+
end
|
88
|
+
rescue ArgumentError, TypeError
|
89
|
+
Time.at(0)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Normalize operation to uppercase string
|
93
|
+
#
|
94
|
+
# @param operation [String, Symbol] operation to normalize
|
95
|
+
# @return [String] uppercase operation string
|
96
|
+
def normalize_operation(operation)
|
97
|
+
operation&.to_s&.upcase
|
98
|
+
end
|
99
|
+
|
100
|
+
# Normalize table name to string
|
101
|
+
#
|
102
|
+
# @param table_name [String, Symbol] table name to normalize
|
103
|
+
# @return [String] normalized table name
|
104
|
+
def normalize_table_name(table_name)
|
105
|
+
table_name&.to_s
|
106
|
+
end
|
107
|
+
|
108
|
+
# Normalize record ID to string
|
109
|
+
#
|
110
|
+
# @param record_id [String, Integer] record ID to normalize
|
111
|
+
# @return [String] normalized record ID
|
112
|
+
def normalize_record_id(record_id)
|
113
|
+
record_id&.to_s
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
# Extracts attributes from objects that don't respond to to_h
|
119
|
+
#
|
120
|
+
# @param object [Object] object to extract attributes from
|
121
|
+
# @return [Hash] extracted attributes hash
|
122
|
+
def extract_object_attributes(object)
|
123
|
+
{
|
124
|
+
id: object.respond_to?(:id) ? object.id : nil,
|
125
|
+
name: object.respond_to?(:name) ? object.name : nil,
|
126
|
+
started_at: object.respond_to?(:started_at) ? object.started_at : nil,
|
127
|
+
ended_at: object.respond_to?(:ended_at) ? object.ended_at : nil,
|
128
|
+
changes: object.respond_to?(:changes) ? object.changes : []
|
129
|
+
}
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Storage
|
5
|
+
module Concerns
|
6
|
+
# Provides standardized error handling capabilities for storage classes
|
7
|
+
#
|
8
|
+
# This concern can be included in storage classes to provide consistent
|
9
|
+
# error handling patterns, logging, and recovery mechanisms.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# class MyStorage < BaseStorage
|
13
|
+
# include Concerns::ErrorHandler
|
14
|
+
#
|
15
|
+
# def risky_operation
|
16
|
+
# safe_operation("my operation") do
|
17
|
+
# # potentially failing code
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
module ErrorHandler
|
22
|
+
# Executes a block with error handling and optional default return value
|
23
|
+
#
|
24
|
+
# @param operation_name [String] description of the operation for logging
|
25
|
+
# @param default_value [Object] value to return if operation fails
|
26
|
+
# @yield [] the block to execute safely
|
27
|
+
# @return [Object] the result of the block or default_value on error
|
28
|
+
def safe_operation(operation_name, default_value = nil, &block)
|
29
|
+
block.call
|
30
|
+
rescue JSON::ParserError => e
|
31
|
+
log_error("JSON parsing failed in #{operation_name}", e)
|
32
|
+
default_value
|
33
|
+
rescue Errno::ENOENT => e
|
34
|
+
log_error("File not found in #{operation_name}", e)
|
35
|
+
default_value
|
36
|
+
rescue Errno::EACCES => e
|
37
|
+
log_error("Permission denied in #{operation_name}", e)
|
38
|
+
raise StorageError, "Permission denied: #{e.message}"
|
39
|
+
rescue StandardError => e
|
40
|
+
log_error("#{operation_name} failed", e)
|
41
|
+
default_value
|
42
|
+
end
|
43
|
+
|
44
|
+
# Executes a block with error handling that raises StorageError on failure
|
45
|
+
#
|
46
|
+
# @param operation [String] description of the operation
|
47
|
+
# @yield [] the block to execute
|
48
|
+
# @return [Object] the result of the block
|
49
|
+
# @raise [StorageError] if the operation fails
|
50
|
+
def with_error_handling(operation, &block)
|
51
|
+
block.call
|
52
|
+
rescue StandardError => e
|
53
|
+
error_message = "Storage #{operation} failed: #{e.message}"
|
54
|
+
log_error(error_message, e)
|
55
|
+
raise StorageError, error_message
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Logs an error message with exception details
|
61
|
+
#
|
62
|
+
# @param message [String] the error message
|
63
|
+
# @param error [Exception] the exception that occurred
|
64
|
+
# @return [void]
|
65
|
+
def log_error(message, error)
|
66
|
+
if defined?(Rails) && Rails.logger
|
67
|
+
Rails.logger.warn("#{message}: #{error.message}")
|
68
|
+
else
|
69
|
+
warn "#{message}: #{error.message}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|