solid_observer 0.1.1 → 0.4.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/CHANGELOG.md +84 -0
- data/README.md +241 -59
- data/app/assets/javascripts/solid_observer/live_poll.js +376 -0
- data/app/assets/stylesheets/solid_observer/application.css +18 -0
- data/app/controllers/concerns/solid_observer/paginatable.rb +17 -0
- data/app/controllers/concerns/solid_observer/require_persistence_mode.rb +19 -0
- data/app/controllers/concerns/solid_observer/require_solid_queue.rb +19 -0
- data/app/controllers/solid_observer/application_controller.rb +69 -0
- data/app/controllers/solid_observer/cache_dashboard_controller.rb +59 -0
- data/app/controllers/solid_observer/cache_operations_controller.rb +24 -0
- data/app/controllers/solid_observer/dashboard_controller.rb +122 -0
- data/app/controllers/solid_observer/events_controller.rb +50 -0
- data/app/controllers/solid_observer/jobs_controller.rb +85 -0
- data/app/controllers/solid_observer/storages_controller.rb +12 -0
- data/app/helpers/solid_observer/application_helper.rb +244 -0
- data/app/helpers/solid_observer/dashboard_helper.rb +58 -0
- data/app/models/solid_observer/cache_event.rb +15 -0
- data/app/models/solid_observer/cache_metric.rb +14 -0
- data/app/models/solid_observer/queue_event.rb +134 -0
- data/app/models/solid_observer/queue_metric.rb +1 -1
- data/app/models/solid_observer/storage_info.rb +4 -1
- data/app/presenters/solid_observer/execution_presenter.rb +50 -0
- data/app/views/layouts/solid_observer/application.html.erb +597 -0
- data/app/views/solid_observer/cache_dashboard/_charts.html.erb +40 -0
- data/app/views/solid_observer/cache_dashboard/_recent_events.html.erb +34 -0
- data/app/views/solid_observer/cache_dashboard/_summary.html.erb +39 -0
- data/app/views/solid_observer/cache_dashboard/index.html.erb +62 -0
- data/app/views/solid_observer/cache_operations/_confirm_clear.html.erb +6 -0
- data/app/views/solid_observer/cache_operations/index.html.erb +60 -0
- data/app/views/solid_observer/dashboard/_chart.html.erb +28 -0
- data/app/views/solid_observer/dashboard/_live_state.html.erb +20 -0
- data/app/views/solid_observer/dashboard/_queue_table.html.erb +34 -0
- data/app/views/solid_observer/dashboard/_right_now.html.erb +3 -0
- data/app/views/solid_observer/dashboard/_throughput.html.erb +32 -0
- data/app/views/solid_observer/dashboard/index.html.erb +143 -0
- data/app/views/solid_observer/errors/storage_unavailable.html.erb +27 -0
- data/app/views/solid_observer/events/index.html.erb +53 -0
- data/app/views/solid_observer/events/show.html.erb +47 -0
- data/app/views/solid_observer/jobs/index.html.erb +61 -0
- data/app/views/solid_observer/jobs/show.html.erb +71 -0
- data/app/views/solid_observer/shared/_empty_state.html.erb +5 -0
- data/app/views/solid_observer/shared/_pagination.html.erb +17 -0
- data/app/views/solid_observer/shared/_stat_card.html.erb +9 -0
- data/app/views/solid_observer/storages/show.html.erb +71 -0
- data/bin/quality_gate +95 -0
- data/config/routes.rb +22 -0
- data/db/migrate/20260424000001_add_composite_indexes_to_queue_events.rb +30 -0
- data/db/migrate/20260601000001_create_solid_observer_cache_events.rb +22 -0
- data/db/migrate/20260601000002_create_solid_observer_cache_metrics.rb +18 -0
- data/db/migrate/20260602000001_add_component_to_solid_observer_storage_infos.rb +8 -0
- data/lib/generators/solid_observer/install_generator.rb +12 -25
- data/lib/generators/solid_observer/templates/initializer.rb.tt +6 -6
- data/lib/solid_observer/base_metric.rb +1 -1
- data/lib/solid_observer/cache_event_buffer.rb +53 -0
- data/lib/solid_observer/cache_subscriber.rb +47 -0
- data/lib/solid_observer/chart_buffer.rb +83 -0
- data/lib/solid_observer/cli/base.rb +2 -2
- data/lib/solid_observer/cli/jobs.rb +2 -2
- data/lib/solid_observer/cli/status.rb +20 -2
- data/lib/solid_observer/cli/storage.rb +48 -44
- data/lib/solid_observer/configuration.rb +67 -38
- data/lib/solid_observer/correlation_id_resolver.rb +8 -6
- data/lib/solid_observer/engine.rb +110 -18
- data/lib/solid_observer/params/events_filter.rb +37 -0
- data/lib/solid_observer/params/jobs_filter.rb +35 -0
- data/lib/solid_observer/queries/events_query.rb +27 -0
- data/lib/solid_observer/queries/execution_finder.rb +42 -0
- data/lib/solid_observer/queries/job_executions_query.rb +73 -0
- data/lib/solid_observer/queue_event_buffer.rb +163 -25
- data/lib/solid_observer/queue_stats.rb +165 -19
- data/lib/solid_observer/services/cache_operations.rb +115 -0
- data/lib/solid_observer/services/cache_stats.rb +329 -0
- data/lib/solid_observer/services/cleanup_storage.rb +73 -41
- data/lib/solid_observer/services/database_size.rb +91 -0
- data/lib/solid_observer/services/flush_cache_event_buffer.rb +54 -0
- data/lib/solid_observer/services/flush_event_buffer.rb +31 -15
- data/lib/solid_observer/services/install_migrations.rb +49 -0
- data/lib/solid_observer/services/record_cache_event.rb +142 -0
- data/lib/solid_observer/services/record_cache_metric.rb +74 -0
- data/lib/solid_observer/services/record_event.rb +51 -14
- data/lib/solid_observer/services/storage_info_snapshot.rb +128 -0
- data/lib/solid_observer/services/ui_auth_check.rb +65 -0
- data/lib/solid_observer/subscriber.rb +15 -8
- data/lib/solid_observer/version.rb +1 -1
- data/lib/solid_observer.rb +7 -0
- data/lib/tasks/solid_observer.rake +39 -2
- metadata +77 -1
|
@@ -3,16 +3,34 @@
|
|
|
3
3
|
module SolidObserver
|
|
4
4
|
module CLI
|
|
5
5
|
class Status < Base
|
|
6
|
+
BANNER_ICON_TOP = " ┌─ ─┐"
|
|
7
|
+
BANNER_ICON_MID_LEFT = " ◉"
|
|
8
|
+
BANNER_ICON_BOT = " └─ ─┘"
|
|
9
|
+
BANNER_NAME = "solid_observer"
|
|
10
|
+
BANNER_NAME_GAP = " "
|
|
11
|
+
|
|
6
12
|
def call
|
|
13
|
+
print_banner
|
|
7
14
|
print_header
|
|
8
15
|
print_queue_stats
|
|
9
16
|
end
|
|
10
17
|
|
|
11
18
|
private
|
|
12
19
|
|
|
20
|
+
def print_banner
|
|
21
|
+
output(BANNER_ICON_TOP, color: :red)
|
|
22
|
+
output(banner_middle_line)
|
|
23
|
+
output(BANNER_ICON_BOT, color: :red)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def banner_middle_line
|
|
27
|
+
icon = color_enabled? ? colorize(BANNER_ICON_MID_LEFT, :red) : BANNER_ICON_MID_LEFT
|
|
28
|
+
"#{icon}#{BANNER_NAME_GAP}#{BANNER_NAME}"
|
|
29
|
+
end
|
|
30
|
+
|
|
13
31
|
def print_header
|
|
14
|
-
output("\n📊 SolidObserver Status", color: :
|
|
15
|
-
output("=" * 50, color: :
|
|
32
|
+
output("\n📊 SolidObserver Status", color: :red)
|
|
33
|
+
output("=" * 50, color: :red)
|
|
16
34
|
output("")
|
|
17
35
|
end
|
|
18
36
|
|
|
@@ -4,70 +4,74 @@ module SolidObserver
|
|
|
4
4
|
module CLI
|
|
5
5
|
class Storage < Base
|
|
6
6
|
def call
|
|
7
|
-
if SolidObserver.config.realtime_mode?
|
|
8
|
-
print_section_header("💾 Storage Status")
|
|
9
|
-
info("Storage monitoring is not available in real-time mode.")
|
|
10
|
-
info("Switch to persistence mode for event history and storage tracking.")
|
|
11
|
-
output("")
|
|
12
|
-
return
|
|
13
|
-
end
|
|
14
|
-
|
|
15
7
|
print_section_header("💾 Storage Status")
|
|
8
|
+
return print_realtime_mode_message if SolidObserver.config.realtime_mode?
|
|
16
9
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if current_stats[:error]
|
|
20
|
-
error(current_stats[:error])
|
|
21
|
-
return
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
print_storage_table(current_stats)
|
|
25
|
-
print_configuration
|
|
10
|
+
render_storage_status
|
|
26
11
|
end
|
|
27
12
|
|
|
28
13
|
private
|
|
29
14
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
max_size_bytes: SolidObserver.config.max_db_size
|
|
35
|
-
}
|
|
36
|
-
rescue => e
|
|
37
|
-
{error: "Failed to gather storage stats: #{e.message}"}
|
|
15
|
+
def print_realtime_mode_message
|
|
16
|
+
info("Storage monitoring is not available in real-time mode.")
|
|
17
|
+
info("Switch to persistence mode for event history and storage tracking.")
|
|
18
|
+
output("")
|
|
38
19
|
end
|
|
39
20
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
|
|
21
|
+
def render_storage_status
|
|
22
|
+
current_stats = gather_storage_stats
|
|
23
|
+
error_message = current_stats[:error]
|
|
24
|
+
return error(error_message) if error_message
|
|
43
25
|
|
|
44
|
-
|
|
26
|
+
print_storage_table(current_stats)
|
|
27
|
+
print_configuration
|
|
28
|
+
end
|
|
45
29
|
|
|
46
|
-
|
|
30
|
+
def gather_storage_stats
|
|
31
|
+
{components: SolidObserver::Services::StorageInfoSnapshot.call, max_size_bytes: SolidObserver.config.max_db_size}
|
|
47
32
|
rescue => e
|
|
48
|
-
|
|
49
|
-
0
|
|
33
|
+
{error: "Failed to gather storage stats: #{e.message}"}
|
|
50
34
|
end
|
|
51
35
|
|
|
52
36
|
def print_storage_table(stats)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
status = status_indicator(percentage)
|
|
37
|
+
max_size_bytes = stats[:max_size_bytes]
|
|
38
|
+
components = stats[:components]
|
|
56
39
|
|
|
57
40
|
table(
|
|
58
41
|
headers: ["Component", "Size", "Events", "Usage", "Status"],
|
|
59
|
-
rows:
|
|
60
|
-
"Queue",
|
|
61
|
-
format_size(size_mb),
|
|
62
|
-
format_number(stats[:event_count]),
|
|
63
|
-
"#{percentage}%",
|
|
64
|
-
status
|
|
65
|
-
]]
|
|
42
|
+
rows: components.map { |component| storage_row(component: component, max_size_bytes: max_size_bytes) }
|
|
66
43
|
)
|
|
67
44
|
|
|
68
45
|
output("")
|
|
69
46
|
end
|
|
70
47
|
|
|
48
|
+
def storage_row(component:, max_size_bytes:)
|
|
49
|
+
event_count = component[:event_count]
|
|
50
|
+
size, usage, status = storage_displays(component: component, max_size_bytes: max_size_bytes)
|
|
51
|
+
|
|
52
|
+
[
|
|
53
|
+
component[:label],
|
|
54
|
+
size,
|
|
55
|
+
event_count ? format_number(event_count) : "—",
|
|
56
|
+
usage,
|
|
57
|
+
status
|
|
58
|
+
]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def storage_displays(component:, max_size_bytes:)
|
|
62
|
+
return unavailable_displays unless component[:available]
|
|
63
|
+
|
|
64
|
+
db_size_bytes = component[:db_size_bytes]
|
|
65
|
+
return ["N/A", "N/A", "— Unknown"] unless db_size_bytes
|
|
66
|
+
|
|
67
|
+
percentage = calculate_percentage(db_size_bytes, max_size_bytes)
|
|
68
|
+
[format_size(bytes_to_mb(db_size_bytes)), "#{percentage}%", status_indicator(percentage)]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def unavailable_displays
|
|
72
|
+
["—", "—", "— Unavailable"]
|
|
73
|
+
end
|
|
74
|
+
|
|
71
75
|
def print_configuration
|
|
72
76
|
retention_days = (SolidObserver.config.event_retention / 1.day).to_i
|
|
73
77
|
max_size_mb = bytes_to_mb(SolidObserver.config.max_db_size)
|
|
@@ -113,8 +117,8 @@ module SolidObserver
|
|
|
113
117
|
|
|
114
118
|
def print_section_header(title)
|
|
115
119
|
output("")
|
|
116
|
-
output(title, color: :
|
|
117
|
-
output("=" * 50, color: :
|
|
120
|
+
output(title, color: :red)
|
|
121
|
+
output("=" * 50, color: :red)
|
|
118
122
|
output("")
|
|
119
123
|
end
|
|
120
124
|
end
|
|
@@ -15,23 +15,24 @@ module SolidObserver
|
|
|
15
15
|
# UI Settings
|
|
16
16
|
attr_accessor :ui_enabled,
|
|
17
17
|
:ui_base_controller,
|
|
18
|
-
:
|
|
19
|
-
:
|
|
20
|
-
:http_basic_auth_password
|
|
18
|
+
:ui_username,
|
|
19
|
+
:ui_password
|
|
21
20
|
|
|
22
21
|
# Observer Settings
|
|
23
22
|
attr_accessor :observe_queue
|
|
24
23
|
|
|
25
|
-
# Observer Settings (planned for
|
|
24
|
+
# Observer Settings (planned for a future release)
|
|
26
25
|
# @note Cache and Cable observers are not yet implemented
|
|
27
26
|
attr_accessor :observe_cache,
|
|
28
27
|
:observe_cable,
|
|
29
|
-
:cache_sampling_rate
|
|
28
|
+
:cache_sampling_rate,
|
|
29
|
+
:cache_slow_threshold,
|
|
30
|
+
:cache_store_errors
|
|
30
31
|
|
|
31
32
|
# Retention Settings
|
|
32
33
|
attr_accessor :event_retention
|
|
33
34
|
|
|
34
|
-
# Retention Settings (planned for
|
|
35
|
+
# Retention Settings (planned for a future release)
|
|
35
36
|
# @note Metrics cleanup is not yet implemented
|
|
36
37
|
attr_accessor :metrics_retention
|
|
37
38
|
|
|
@@ -45,46 +46,31 @@ module SolidObserver
|
|
|
45
46
|
attr_reader :sampling_rate,
|
|
46
47
|
:warning_threshold,
|
|
47
48
|
:buffer_size,
|
|
48
|
-
:flush_interval
|
|
49
|
+
:flush_interval,
|
|
50
|
+
:max_buffer_size,
|
|
51
|
+
:buffer_overflow_strategy,
|
|
52
|
+
:filter_cache_ttl
|
|
49
53
|
|
|
50
54
|
# Correlation Settings
|
|
51
55
|
attr_accessor :correlation_id_generator
|
|
52
56
|
|
|
53
57
|
def initialize
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
@observe_cache = false
|
|
67
|
-
@observe_cable = false
|
|
68
|
-
|
|
69
|
-
# Retention defaults
|
|
70
|
-
@event_retention = 30.days
|
|
71
|
-
@metrics_retention = 90.days
|
|
72
|
-
|
|
73
|
-
# Storage defaults
|
|
74
|
-
@max_db_size = 1.gigabyte
|
|
75
|
-
@warning_threshold = 0.8
|
|
76
|
-
|
|
77
|
-
# Performance defaults
|
|
78
|
-
@sampling_rate = 1.0
|
|
79
|
-
@cache_sampling_rate = 0.1
|
|
80
|
-
@buffer_size = 1000
|
|
81
|
-
@flush_interval = 10.seconds
|
|
82
|
-
|
|
83
|
-
# Correlation defaults
|
|
84
|
-
@correlation_id_generator = nil
|
|
58
|
+
@ui_enabled, @ui_base_controller, @ui_username, @ui_password,
|
|
59
|
+
@storage_mode, @observe_queue, @observe_cache, @observe_cable,
|
|
60
|
+
@event_retention, @metrics_retention, @max_db_size, @warning_threshold,
|
|
61
|
+
@sampling_rate, @cache_sampling_rate, @cache_slow_threshold, @cache_store_errors,
|
|
62
|
+
@buffer_size, @flush_interval,
|
|
63
|
+
@max_buffer_size, @buffer_overflow_strategy, @filter_cache_ttl,
|
|
64
|
+
@correlation_id_generator = !production?, "::ApplicationController", nil, nil,
|
|
65
|
+
:persistence, true, false, false,
|
|
66
|
+
30.days, 90.days, 1.gigabyte, 0.8,
|
|
67
|
+
1.0, 0.1, 0.1, true, 1000, 10.seconds,
|
|
68
|
+
10_000, :drop_old, 1.minute,
|
|
69
|
+
nil
|
|
85
70
|
end
|
|
86
71
|
|
|
87
72
|
STORAGE_MODES = %i[persistence realtime].freeze
|
|
73
|
+
BUFFER_OVERFLOW_STRATEGIES = %i[drop_old drop_new].freeze
|
|
88
74
|
|
|
89
75
|
def storage_mode=(value)
|
|
90
76
|
value = value.to_sym
|
|
@@ -101,6 +87,22 @@ module SolidObserver
|
|
|
101
87
|
@storage_mode == :realtime
|
|
102
88
|
end
|
|
103
89
|
|
|
90
|
+
def solid_queue_available?
|
|
91
|
+
!!defined?(::SolidQueue)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def solid_cache_available?
|
|
95
|
+
!!defined?(::SolidCache)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def solid_queue_enabled?
|
|
99
|
+
observe_queue
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def solid_cache_enabled?
|
|
103
|
+
observe_cache && solid_cache_available?
|
|
104
|
+
end
|
|
105
|
+
|
|
104
106
|
def sampling_rate=(value)
|
|
105
107
|
validate_rate!(:sampling_rate, value)
|
|
106
108
|
@sampling_rate = value
|
|
@@ -113,6 +115,10 @@ module SolidObserver
|
|
|
113
115
|
|
|
114
116
|
def buffer_size=(value)
|
|
115
117
|
validate_positive_integer!(:buffer_size, value)
|
|
118
|
+
if defined?(@max_buffer_size) && value > @max_buffer_size
|
|
119
|
+
raise ArgumentError, "buffer_size must be <= max_buffer_size"
|
|
120
|
+
end
|
|
121
|
+
|
|
116
122
|
@buffer_size = value
|
|
117
123
|
end
|
|
118
124
|
|
|
@@ -121,6 +127,29 @@ module SolidObserver
|
|
|
121
127
|
@flush_interval = value
|
|
122
128
|
end
|
|
123
129
|
|
|
130
|
+
def max_buffer_size=(value)
|
|
131
|
+
validate_positive_integer!(:max_buffer_size, value)
|
|
132
|
+
if defined?(@buffer_size) && value < @buffer_size
|
|
133
|
+
raise ArgumentError, "max_buffer_size must be >= buffer_size"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
@max_buffer_size = value
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def buffer_overflow_strategy=(value)
|
|
140
|
+
value = value.to_sym
|
|
141
|
+
unless BUFFER_OVERFLOW_STRATEGIES.include?(value)
|
|
142
|
+
raise ArgumentError, "buffer_overflow_strategy must be :drop_old or :drop_new"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
@buffer_overflow_strategy = value
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def filter_cache_ttl=(value)
|
|
149
|
+
validate_positive_numeric!(:filter_cache_ttl, value)
|
|
150
|
+
@filter_cache_ttl = value
|
|
151
|
+
end
|
|
152
|
+
|
|
124
153
|
private
|
|
125
154
|
|
|
126
155
|
def validate_rate!(name, value)
|
|
@@ -38,12 +38,7 @@ module SolidObserver
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def call_custom_generator
|
|
41
|
-
|
|
42
|
-
return nil if result.blank?
|
|
43
|
-
result
|
|
44
|
-
rescue => e
|
|
45
|
-
log_generator_error(e)
|
|
46
|
-
nil
|
|
41
|
+
custom_generator_value.presence
|
|
47
42
|
end
|
|
48
43
|
|
|
49
44
|
def log_generator_error(exception)
|
|
@@ -58,5 +53,12 @@ module SolidObserver
|
|
|
58
53
|
def extract_job_id
|
|
59
54
|
@event.payload[:job].job_id
|
|
60
55
|
end
|
|
56
|
+
|
|
57
|
+
def custom_generator_value
|
|
58
|
+
SolidObserver.config.correlation_id_generator.call
|
|
59
|
+
rescue => e
|
|
60
|
+
log_generator_error(e)
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
61
63
|
end
|
|
62
64
|
end
|
|
@@ -1,26 +1,68 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "cache_event_buffer"
|
|
4
|
+
require_relative "cache_subscriber"
|
|
5
|
+
require_relative "services/record_cache_event"
|
|
6
|
+
require_relative "services/record_cache_metric"
|
|
7
|
+
require_relative "services/flush_cache_event_buffer"
|
|
8
|
+
require_relative "services/cache_stats"
|
|
9
|
+
require_relative "services/cache_operations"
|
|
10
|
+
|
|
3
11
|
module SolidObserver
|
|
4
12
|
class Engine < ::Rails::Engine
|
|
5
13
|
isolate_namespace SolidObserver
|
|
6
14
|
|
|
15
|
+
SOLID_QUEUE_AVAILABLE = defined?(::SolidQueue)
|
|
16
|
+
SOLID_CACHE_AVAILABLE = defined?(::SolidCache)
|
|
17
|
+
|
|
18
|
+
middleware.use ActionDispatch::Cookies
|
|
19
|
+
middleware.use ActionDispatch::Session::CookieStore, key: "_solid_observer_session"
|
|
20
|
+
middleware.use ActionDispatch::Flash
|
|
21
|
+
|
|
7
22
|
class << self
|
|
8
23
|
def check_solid_queue_availability
|
|
9
|
-
return if defined?(SolidQueue)
|
|
24
|
+
return if defined?(::SolidQueue)
|
|
10
25
|
|
|
11
26
|
Rails.logger.warn "[SolidObserver] SolidQueue not detected. Queue observability features will be limited."
|
|
12
27
|
end
|
|
13
28
|
|
|
29
|
+
def check_solid_cache_availability
|
|
30
|
+
return if defined?(::SolidCache)
|
|
31
|
+
return unless SolidObserver.config.observe_cache
|
|
32
|
+
|
|
33
|
+
Rails.logger.warn "[SolidObserver] SolidCache not detected. Cache observability features will be disabled."
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def check_ui_authentication
|
|
37
|
+
Services::UiAuthCheck.call(config: SolidObserver.config)
|
|
38
|
+
end
|
|
39
|
+
|
|
14
40
|
def configure_database_connection
|
|
15
41
|
return if SolidObserver.config.realtime_mode?
|
|
42
|
+
return unless queue_db_config
|
|
43
|
+
|
|
44
|
+
connect_observer_models
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def activate_subscribers
|
|
48
|
+
return activate_subscribers_in_realtime if SolidObserver.config.realtime_mode?
|
|
49
|
+
return if activation_skipped_for_table_status_for_enabled_components?
|
|
50
|
+
|
|
51
|
+
Rails.logger.info "[SolidObserver] Activating event subscribers"
|
|
52
|
+
Subscriber.subscribe! if should_activate_queue_subscriber?
|
|
53
|
+
CacheSubscriber.subscribe! if should_activate_cache_subscriber?
|
|
54
|
+
end
|
|
16
55
|
|
|
17
|
-
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def queue_db_config
|
|
59
|
+
ActiveRecord::Base.configurations.configs_for(
|
|
18
60
|
env_name: Rails.env,
|
|
19
61
|
name: "solid_observer_queue"
|
|
20
62
|
)
|
|
63
|
+
end
|
|
21
64
|
|
|
22
|
-
|
|
23
|
-
|
|
65
|
+
def connect_observer_models
|
|
24
66
|
connection_config = {
|
|
25
67
|
database: {writing: :solid_observer_queue, reading: :solid_observer_queue}
|
|
26
68
|
}
|
|
@@ -29,37 +71,87 @@ module SolidObserver
|
|
|
29
71
|
SolidObserver::BaseMetric.connects_to(**connection_config)
|
|
30
72
|
end
|
|
31
73
|
|
|
32
|
-
def
|
|
33
|
-
logger
|
|
74
|
+
def activate_subscribers_in_realtime
|
|
75
|
+
Rails.logger.info "[SolidObserver] Starting in real-time mode (no persistence)"
|
|
76
|
+
Subscriber.subscribe! if should_activate_queue_subscriber?
|
|
77
|
+
CacheSubscriber.subscribe! if should_activate_cache_subscriber?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def activation_skipped_for_table_status_for_enabled_components?
|
|
81
|
+
enabled_tables = []
|
|
82
|
+
enabled_tables << "solid_observer_queue_events" if should_activate_queue_subscriber?
|
|
83
|
+
enabled_tables << "solid_observer_cache_events" if should_activate_cache_subscriber?
|
|
84
|
+
|
|
85
|
+
enabled_tables.any? { |table_name| skip_activation_for_missing_table?(table_name) }
|
|
86
|
+
end
|
|
34
87
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
88
|
+
def skip_activation_for_missing_table?(table_name)
|
|
89
|
+
case table_status(table_name)
|
|
90
|
+
when :absent
|
|
91
|
+
log_activation_skip("Tables not found (missing: #{table_name}). Run: rails solid_observer:install:migrations && rails db:migrate")
|
|
92
|
+
true
|
|
93
|
+
when :unknown
|
|
94
|
+
log_activation_skip("Database not reachable at boot. Skipping subscriber activation.")
|
|
95
|
+
true
|
|
40
96
|
else
|
|
41
|
-
|
|
97
|
+
false
|
|
42
98
|
end
|
|
99
|
+
end
|
|
43
100
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
logger.info "[SolidObserver] Database not ready yet. Skipping subscriber activation."
|
|
101
|
+
def should_activate_queue_subscriber?
|
|
102
|
+
SolidObserver.config.solid_queue_enabled?
|
|
47
103
|
end
|
|
48
104
|
|
|
49
|
-
|
|
105
|
+
def should_activate_cache_subscriber?
|
|
106
|
+
SolidObserver.config.solid_cache_enabled?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def log_activation_skip(message)
|
|
110
|
+
Rails.logger.info("[SolidObserver] #{message}")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def table_status(table_name)
|
|
114
|
+
pool = SolidObserver::BaseEvent.connection_pool
|
|
115
|
+
|
|
116
|
+
return :present if cached_data_source_exists?(pool, table_name)
|
|
117
|
+
|
|
118
|
+
data_source_exists_in_db?(pool, table_name) ? :present : :absent
|
|
119
|
+
rescue *boot_connection_errors
|
|
120
|
+
:unknown
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def cached_data_source_exists?(pool, table_name)
|
|
124
|
+
cache = pool.schema_cache
|
|
125
|
+
cache.data_source_exists?(pool, table_name)
|
|
126
|
+
rescue ArgumentError
|
|
127
|
+
cache.data_source_exists?(table_name)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def data_source_exists_in_db?(pool, table_name)
|
|
131
|
+
pool.with_connection { |connection| connection.data_source_exists?(table_name) }
|
|
132
|
+
end
|
|
50
133
|
|
|
51
|
-
def
|
|
52
|
-
|
|
134
|
+
def boot_connection_errors
|
|
135
|
+
[
|
|
136
|
+
ActiveRecord::NoDatabaseError,
|
|
137
|
+
ActiveRecord::ConnectionNotEstablished,
|
|
138
|
+
ActiveRecord::StatementInvalid,
|
|
139
|
+
*([PG::ConnectionBad] if defined?(PG::ConnectionBad)),
|
|
140
|
+
*([Mysql2::Error::ConnectionError] if defined?(Mysql2::Error::ConnectionError)),
|
|
141
|
+
*([SQLite3::CantOpenException] if defined?(SQLite3::CantOpenException))
|
|
142
|
+
]
|
|
53
143
|
end
|
|
54
144
|
end
|
|
55
145
|
|
|
56
146
|
config.before_initialize do
|
|
57
147
|
Engine.check_solid_queue_availability
|
|
148
|
+
Engine.check_solid_cache_availability
|
|
58
149
|
end
|
|
59
150
|
|
|
60
151
|
config.after_initialize do
|
|
61
152
|
Engine.configure_database_connection
|
|
62
153
|
Engine.activate_subscribers
|
|
154
|
+
Engine.check_ui_authentication
|
|
63
155
|
end
|
|
64
156
|
end
|
|
65
157
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
module Params
|
|
5
|
+
class EventsFilter
|
|
6
|
+
def self.from_params(params)
|
|
7
|
+
new(
|
|
8
|
+
event_type: params[:event_type].presence,
|
|
9
|
+
job_class: params[:job_class].presence,
|
|
10
|
+
queue_name: params[:queue_name].presence,
|
|
11
|
+
from: parse_date(params[:from]),
|
|
12
|
+
to: parse_date(params[:to]),
|
|
13
|
+
page: (params[:page].presence || 1).to_i
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def parse_date(date_string)
|
|
21
|
+
return nil if date_string.blank?
|
|
22
|
+
|
|
23
|
+
Date.parse(date_string)
|
|
24
|
+
rescue ArgumentError
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
attr_reader :event_type, :job_class, :queue_name, :from, :to, :page
|
|
30
|
+
|
|
31
|
+
def initialize(event_type:, job_class:, queue_name:, from:, to:, page:)
|
|
32
|
+
@event_type, @job_class, @queue_name = event_type, job_class, queue_name
|
|
33
|
+
@from, @to, @page = from, to, page
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
module Params
|
|
5
|
+
class JobsFilter
|
|
6
|
+
ALLOWED_STATUSES = %w[ready scheduled claimed failed].freeze
|
|
7
|
+
PSEUDO_STATUSES = %w[all_active].freeze
|
|
8
|
+
|
|
9
|
+
def self.from_params(params)
|
|
10
|
+
new(
|
|
11
|
+
status: params[:status].presence || "all_active",
|
|
12
|
+
queue_name: params[:queue_name].presence,
|
|
13
|
+
job_class: params[:job_class].presence,
|
|
14
|
+
page: (params[:page].presence || 1).to_i
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :status, :queue_name, :job_class, :page
|
|
19
|
+
|
|
20
|
+
def initialize(status:, queue_name:, job_class:, page:)
|
|
21
|
+
@status = normalize_status(status)
|
|
22
|
+
@queue_name = queue_name
|
|
23
|
+
@job_class = job_class
|
|
24
|
+
@page = page
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def normalize_status(status)
|
|
30
|
+
normalized = status.to_s.downcase
|
|
31
|
+
(ALLOWED_STATUSES + PSEUDO_STATUSES).include?(normalized) ? normalized : "all_active"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
module Queries
|
|
5
|
+
class EventsQuery
|
|
6
|
+
def initialize(filter)
|
|
7
|
+
@filter = filter
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
event_type = @filter.event_type
|
|
12
|
+
job_class = @filter.job_class
|
|
13
|
+
queue_name = @filter.queue_name
|
|
14
|
+
from = @filter.from
|
|
15
|
+
to = @filter.to
|
|
16
|
+
|
|
17
|
+
scope = QueueEvent.order(recorded_at: :desc)
|
|
18
|
+
scope = scope.by_event_type(event_type) if event_type.present?
|
|
19
|
+
scope = scope.by_job_class(job_class) if job_class.present?
|
|
20
|
+
scope = scope.by_queue(queue_name) if queue_name.present?
|
|
21
|
+
scope = scope.since(from.beginning_of_day) if from
|
|
22
|
+
scope = scope.before(to.end_of_day) if to
|
|
23
|
+
scope
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
module Queries
|
|
5
|
+
class ExecutionFinder
|
|
6
|
+
EXECUTION_TYPES = [
|
|
7
|
+
"SolidQueue::ReadyExecution",
|
|
8
|
+
"SolidQueue::ScheduledExecution",
|
|
9
|
+
"SolidQueue::ClaimedExecution",
|
|
10
|
+
"SolidQueue::FailedExecution"
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
STATUS_TO_EXECUTION_TYPE = {
|
|
14
|
+
"ready" => "SolidQueue::ReadyExecution",
|
|
15
|
+
"scheduled" => "SolidQueue::ScheduledExecution",
|
|
16
|
+
"claimed" => "SolidQueue::ClaimedExecution",
|
|
17
|
+
"failed" => "SolidQueue::FailedExecution"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def self.find_any(id)
|
|
21
|
+
EXECUTION_TYPES.each do |const_name|
|
|
22
|
+
execution = const_name.safe_constantize&.find_by(id: id)
|
|
23
|
+
return execution if execution
|
|
24
|
+
end
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.find_by_status(id, status)
|
|
29
|
+
const_name = STATUS_TO_EXECUTION_TYPE[status.to_s.downcase]
|
|
30
|
+
return nil unless const_name
|
|
31
|
+
|
|
32
|
+
const_name.safe_constantize&.find_by(id: id)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.find_failed(id)
|
|
36
|
+
return nil unless defined?(SolidQueue::FailedExecution)
|
|
37
|
+
|
|
38
|
+
SolidQueue::FailedExecution.find_by(id: id)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|