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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +84 -0
  3. data/README.md +241 -59
  4. data/app/assets/javascripts/solid_observer/live_poll.js +376 -0
  5. data/app/assets/stylesheets/solid_observer/application.css +18 -0
  6. data/app/controllers/concerns/solid_observer/paginatable.rb +17 -0
  7. data/app/controllers/concerns/solid_observer/require_persistence_mode.rb +19 -0
  8. data/app/controllers/concerns/solid_observer/require_solid_queue.rb +19 -0
  9. data/app/controllers/solid_observer/application_controller.rb +69 -0
  10. data/app/controllers/solid_observer/cache_dashboard_controller.rb +59 -0
  11. data/app/controllers/solid_observer/cache_operations_controller.rb +24 -0
  12. data/app/controllers/solid_observer/dashboard_controller.rb +122 -0
  13. data/app/controllers/solid_observer/events_controller.rb +50 -0
  14. data/app/controllers/solid_observer/jobs_controller.rb +85 -0
  15. data/app/controllers/solid_observer/storages_controller.rb +12 -0
  16. data/app/helpers/solid_observer/application_helper.rb +244 -0
  17. data/app/helpers/solid_observer/dashboard_helper.rb +58 -0
  18. data/app/models/solid_observer/cache_event.rb +15 -0
  19. data/app/models/solid_observer/cache_metric.rb +14 -0
  20. data/app/models/solid_observer/queue_event.rb +134 -0
  21. data/app/models/solid_observer/queue_metric.rb +1 -1
  22. data/app/models/solid_observer/storage_info.rb +4 -1
  23. data/app/presenters/solid_observer/execution_presenter.rb +50 -0
  24. data/app/views/layouts/solid_observer/application.html.erb +597 -0
  25. data/app/views/solid_observer/cache_dashboard/_charts.html.erb +40 -0
  26. data/app/views/solid_observer/cache_dashboard/_recent_events.html.erb +34 -0
  27. data/app/views/solid_observer/cache_dashboard/_summary.html.erb +39 -0
  28. data/app/views/solid_observer/cache_dashboard/index.html.erb +62 -0
  29. data/app/views/solid_observer/cache_operations/_confirm_clear.html.erb +6 -0
  30. data/app/views/solid_observer/cache_operations/index.html.erb +60 -0
  31. data/app/views/solid_observer/dashboard/_chart.html.erb +28 -0
  32. data/app/views/solid_observer/dashboard/_live_state.html.erb +20 -0
  33. data/app/views/solid_observer/dashboard/_queue_table.html.erb +34 -0
  34. data/app/views/solid_observer/dashboard/_right_now.html.erb +3 -0
  35. data/app/views/solid_observer/dashboard/_throughput.html.erb +32 -0
  36. data/app/views/solid_observer/dashboard/index.html.erb +143 -0
  37. data/app/views/solid_observer/errors/storage_unavailable.html.erb +27 -0
  38. data/app/views/solid_observer/events/index.html.erb +53 -0
  39. data/app/views/solid_observer/events/show.html.erb +47 -0
  40. data/app/views/solid_observer/jobs/index.html.erb +61 -0
  41. data/app/views/solid_observer/jobs/show.html.erb +71 -0
  42. data/app/views/solid_observer/shared/_empty_state.html.erb +5 -0
  43. data/app/views/solid_observer/shared/_pagination.html.erb +17 -0
  44. data/app/views/solid_observer/shared/_stat_card.html.erb +9 -0
  45. data/app/views/solid_observer/storages/show.html.erb +71 -0
  46. data/bin/quality_gate +95 -0
  47. data/config/routes.rb +22 -0
  48. data/db/migrate/20260424000001_add_composite_indexes_to_queue_events.rb +30 -0
  49. data/db/migrate/20260601000001_create_solid_observer_cache_events.rb +22 -0
  50. data/db/migrate/20260601000002_create_solid_observer_cache_metrics.rb +18 -0
  51. data/db/migrate/20260602000001_add_component_to_solid_observer_storage_infos.rb +8 -0
  52. data/lib/generators/solid_observer/install_generator.rb +12 -25
  53. data/lib/generators/solid_observer/templates/initializer.rb.tt +6 -6
  54. data/lib/solid_observer/base_metric.rb +1 -1
  55. data/lib/solid_observer/cache_event_buffer.rb +53 -0
  56. data/lib/solid_observer/cache_subscriber.rb +47 -0
  57. data/lib/solid_observer/chart_buffer.rb +83 -0
  58. data/lib/solid_observer/cli/base.rb +2 -2
  59. data/lib/solid_observer/cli/jobs.rb +2 -2
  60. data/lib/solid_observer/cli/status.rb +20 -2
  61. data/lib/solid_observer/cli/storage.rb +48 -44
  62. data/lib/solid_observer/configuration.rb +67 -38
  63. data/lib/solid_observer/correlation_id_resolver.rb +8 -6
  64. data/lib/solid_observer/engine.rb +110 -18
  65. data/lib/solid_observer/params/events_filter.rb +37 -0
  66. data/lib/solid_observer/params/jobs_filter.rb +35 -0
  67. data/lib/solid_observer/queries/events_query.rb +27 -0
  68. data/lib/solid_observer/queries/execution_finder.rb +42 -0
  69. data/lib/solid_observer/queries/job_executions_query.rb +73 -0
  70. data/lib/solid_observer/queue_event_buffer.rb +163 -25
  71. data/lib/solid_observer/queue_stats.rb +165 -19
  72. data/lib/solid_observer/services/cache_operations.rb +115 -0
  73. data/lib/solid_observer/services/cache_stats.rb +329 -0
  74. data/lib/solid_observer/services/cleanup_storage.rb +73 -41
  75. data/lib/solid_observer/services/database_size.rb +91 -0
  76. data/lib/solid_observer/services/flush_cache_event_buffer.rb +54 -0
  77. data/lib/solid_observer/services/flush_event_buffer.rb +31 -15
  78. data/lib/solid_observer/services/install_migrations.rb +49 -0
  79. data/lib/solid_observer/services/record_cache_event.rb +142 -0
  80. data/lib/solid_observer/services/record_cache_metric.rb +74 -0
  81. data/lib/solid_observer/services/record_event.rb +51 -14
  82. data/lib/solid_observer/services/storage_info_snapshot.rb +128 -0
  83. data/lib/solid_observer/services/ui_auth_check.rb +65 -0
  84. data/lib/solid_observer/subscriber.rb +15 -8
  85. data/lib/solid_observer/version.rb +1 -1
  86. data/lib/solid_observer.rb +7 -0
  87. data/lib/tasks/solid_observer.rake +39 -2
  88. metadata +77 -1
@@ -186,8 +186,8 @@ module SolidObserver
186
186
  end
187
187
 
188
188
  def print_section_header(title)
189
- output("\n#{title}", color: :cyan)
190
- output("=" * 80, color: :cyan)
189
+ output("\n#{title}", color: :red)
190
+ output("=" * 80, color: :red)
191
191
  output("")
192
192
  end
193
193
  end
@@ -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: :cyan)
15
- output("=" * 50, color: :cyan)
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
- current_stats = gather_storage_stats
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 gather_storage_stats
31
- {
32
- db_size_bytes: calculate_database_size,
33
- event_count: QueueEvent.count,
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 calculate_database_size
41
- db_config = QueueEvent.connection_db_config
42
- db_path = db_config.database
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
- return 0 unless File.exist?(db_path)
26
+ print_storage_table(current_stats)
27
+ print_configuration
28
+ end
45
29
 
46
- File.size(db_path)
30
+ def gather_storage_stats
31
+ {components: SolidObserver::Services::StorageInfoSnapshot.call, max_size_bytes: SolidObserver.config.max_db_size}
47
32
  rescue => e
48
- warning("Could not calculate database size: #{e.message}")
49
- 0
33
+ {error: "Failed to gather storage stats: #{e.message}"}
50
34
  end
51
35
 
52
36
  def print_storage_table(stats)
53
- size_mb = bytes_to_mb(stats[:db_size_bytes])
54
- percentage = calculate_percentage(stats[:db_size_bytes], stats[:max_size_bytes])
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: :cyan)
117
- output("=" * 50, color: :cyan)
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
- :http_basic_auth_enabled,
19
- :http_basic_auth_user,
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 v0.2.0)
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 v0.2.0)
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
- # UI defaults
55
- @ui_enabled = !production?
56
- @ui_base_controller = "::ApplicationController"
57
- @http_basic_auth_enabled = false
58
- @http_basic_auth_user = nil
59
- @http_basic_auth_password = nil
60
-
61
- # Storage mode
62
- @storage_mode = :persistence
63
-
64
- # Observer defaults
65
- @observe_queue = true
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
- result = SolidObserver.config.correlation_id_generator.call
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
- db_config = ActiveRecord::Base.configurations.configs_for(
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
- return unless db_config
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 activate_subscribers
33
- logger = Rails.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
- if SolidObserver.config.realtime_mode?
36
- logger.info "[SolidObserver] Starting in real-time mode (no persistence)"
37
- elsif !table_exists?("solid_observer_queue_events")
38
- logger.info "[SolidObserver] Tables not found. Run: rails solid_observer:install:migrations && rails db:migrate"
39
- return
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
- logger.info "[SolidObserver] Activating event subscribers"
97
+ false
42
98
  end
99
+ end
43
100
 
44
- Subscriber.subscribe!
45
- rescue ActiveRecord::NoDatabaseError
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
- private
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 table_exists?(table_name)
52
- ActiveRecord::Base.connection.table_exists?(table_name)
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