solid_observer 0.1.0 → 0.3.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +73 -0
  3. data/README.md +198 -36
  4. data/app/assets/javascripts/solid_observer/live_poll.js +376 -0
  5. data/app/controllers/concerns/solid_observer/paginatable.rb +17 -0
  6. data/app/controllers/concerns/solid_observer/require_persistence_mode.rb +19 -0
  7. data/app/controllers/concerns/solid_observer/require_solid_queue.rb +19 -0
  8. data/app/controllers/solid_observer/application_controller.rb +69 -0
  9. data/app/controllers/solid_observer/dashboard_controller.rb +79 -0
  10. data/app/controllers/solid_observer/events_controller.rb +50 -0
  11. data/app/controllers/solid_observer/jobs_controller.rb +85 -0
  12. data/app/controllers/solid_observer/storages_controller.rb +12 -0
  13. data/app/helpers/solid_observer/application_helper.rb +95 -0
  14. data/app/helpers/solid_observer/dashboard_helper.rb +39 -0
  15. data/app/models/solid_observer/queue_event.rb +134 -0
  16. data/app/models/solid_observer/queue_metric.rb +1 -1
  17. data/app/presenters/solid_observer/execution_presenter.rb +50 -0
  18. data/app/views/layouts/solid_observer/application.html.erb +470 -0
  19. data/app/views/solid_observer/dashboard/_chart.html.erb +28 -0
  20. data/app/views/solid_observer/dashboard/_live_state.html.erb +20 -0
  21. data/app/views/solid_observer/dashboard/_queue_table.html.erb +34 -0
  22. data/app/views/solid_observer/dashboard/_right_now.html.erb +3 -0
  23. data/app/views/solid_observer/dashboard/_throughput.html.erb +32 -0
  24. data/app/views/solid_observer/dashboard/index.html.erb +113 -0
  25. data/app/views/solid_observer/errors/storage_unavailable.html.erb +27 -0
  26. data/app/views/solid_observer/events/index.html.erb +53 -0
  27. data/app/views/solid_observer/events/show.html.erb +47 -0
  28. data/app/views/solid_observer/jobs/index.html.erb +61 -0
  29. data/app/views/solid_observer/jobs/show.html.erb +71 -0
  30. data/app/views/solid_observer/shared/_empty_state.html.erb +5 -0
  31. data/app/views/solid_observer/shared/_pagination.html.erb +17 -0
  32. data/app/views/solid_observer/shared/_stat_card.html.erb +9 -0
  33. data/app/views/solid_observer/storages/show.html.erb +39 -0
  34. data/bin/quality_gate +95 -0
  35. data/config/routes.rb +17 -0
  36. data/db/migrate/20260424000001_add_composite_indexes_to_queue_events.rb +30 -0
  37. data/lib/generators/solid_observer/install_generator.rb +12 -25
  38. data/lib/generators/solid_observer/templates/initializer.rb.tt +5 -6
  39. data/lib/solid_observer/base_metric.rb +1 -1
  40. data/lib/solid_observer/chart_buffer.rb +83 -0
  41. data/lib/solid_observer/cli/base.rb +2 -2
  42. data/lib/solid_observer/cli/jobs.rb +2 -2
  43. data/lib/solid_observer/cli/status.rb +20 -2
  44. data/lib/solid_observer/cli/storage.rb +41 -32
  45. data/lib/solid_observer/configuration.rb +67 -34
  46. data/lib/solid_observer/correlation_id_resolver.rb +8 -6
  47. data/lib/solid_observer/engine.rb +75 -15
  48. data/lib/solid_observer/params/events_filter.rb +37 -0
  49. data/lib/solid_observer/params/jobs_filter.rb +35 -0
  50. data/lib/solid_observer/queries/events_query.rb +27 -0
  51. data/lib/solid_observer/queries/execution_finder.rb +42 -0
  52. data/lib/solid_observer/queries/job_executions_query.rb +73 -0
  53. data/lib/solid_observer/queue_event_buffer.rb +163 -22
  54. data/lib/solid_observer/queue_stats.rb +165 -19
  55. data/lib/solid_observer/services/cleanup_storage.rb +60 -42
  56. data/lib/solid_observer/services/database_size.rb +86 -0
  57. data/lib/solid_observer/services/flush_event_buffer.rb +31 -15
  58. data/lib/solid_observer/services/install_migrations.rb +49 -0
  59. data/lib/solid_observer/services/record_event.rb +53 -14
  60. data/lib/solid_observer/services/ui_auth_check.rb +65 -0
  61. data/lib/solid_observer/subscriber.rb +15 -8
  62. data/lib/solid_observer/version.rb +1 -1
  63. data/lib/solid_observer.rb +7 -0
  64. data/lib/tasks/solid_observer.rake +10 -2
  65. metadata +55 -1
@@ -9,10 +9,9 @@ SolidObserver.configure do |config|
9
9
  # Recommended: false in production, true in development/staging
10
10
  config.ui_enabled = !Rails.env.production?
11
11
 
12
- # Authentication for UI (when ui_enabled = true)
13
- # config.http_basic_auth_enabled = true
14
- # config.http_basic_auth_user = "admin"
15
- # config.http_basic_auth_password = "secret"
12
+ # Authentication for web UI set a username to enable HTTP Basic Auth
13
+ # config.ui_username = "admin"
14
+ # config.ui_password = "secret"
16
15
 
17
16
  # Base controller for UI (customize authorization)
18
17
  # config.ui_base_controller = "ApplicationController"
@@ -20,11 +19,11 @@ SolidObserver.configure do |config|
20
19
  # === Queue Observability (v0.1.0) ===
21
20
  config.observe_queue = true
22
21
 
23
- # === Cache Observability (Coming in v0.2.0+) ===
22
+ # === Cache Observability (Coming in v0.4.0+) ===
24
23
  # config.observe_cache = true
25
24
  # config.cache_sampling_rate = 0.1 # Sample 10% of cache operations
26
25
 
27
- # === Cable Observability (Coming in v0.2.0+) ===
26
+ # === Cable Observability (Coming in v0.5.0+) ===
28
27
  # config.observe_cable = true
29
28
 
30
29
  # Data Retention
@@ -3,7 +3,7 @@
3
3
  module SolidObserver
4
4
  # BaseMetric provides the foundation for time-series metrics storage.
5
5
  #
6
- # NOTE: Metrics functionality is planned for v0.2.0. The database connection
6
+ # NOTE: Metrics functionality is planned for a future release. The database connection
7
7
  # will be configured by the Engine (similar to BaseEvent) when metrics are
8
8
  # fully implemented.
9
9
  #
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidObserver
4
+ class ChartBuffer
5
+ INSTANCE_MUTEX = Mutex.new
6
+
7
+ class << self
8
+ def append(value, at: Time.now)
9
+ instance.append(value, at: at)
10
+ end
11
+
12
+ def recent(window_seconds)
13
+ instance.recent(window_seconds)
14
+ end
15
+
16
+ def clear
17
+ instance.clear
18
+ end
19
+
20
+ private
21
+
22
+ def instance
23
+ INSTANCE_MUTEX.synchronize { @instance ||= new }
24
+ end
25
+ end
26
+
27
+ def initialize
28
+ @mutex = Mutex.new
29
+ @samples = []
30
+ @cap = nil
31
+ end
32
+
33
+ def append(value, at: Time.now)
34
+ sample = {t: at.to_i, v: value.to_i}
35
+
36
+ @mutex.synchronize { store_sample(sample) }
37
+
38
+ sample
39
+ end
40
+
41
+ def recent(window_seconds)
42
+ cutoff = Time.now.to_i - window_seconds.to_i
43
+
44
+ @mutex.synchronize do
45
+ @samples.select { |sample| sample[:t] >= cutoff }.map(&:dup)
46
+ end
47
+ end
48
+
49
+ def clear
50
+ @mutex.synchronize do
51
+ @samples.clear
52
+ @cap = nil
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def store_sample(sample)
59
+ @cap ||= compute_cap
60
+ replace_or_append(sample)
61
+ trim_to_cap
62
+ end
63
+
64
+ def replace_or_append(sample)
65
+ latest_sample = @samples.last
66
+
67
+ if latest_sample && latest_sample[:t] == sample[:t]
68
+ @samples[-1] = sample
69
+ else
70
+ @samples << sample
71
+ end
72
+ end
73
+
74
+ def trim_to_cap
75
+ overflow = @samples.length - @cap
76
+ @samples.shift(overflow) if overflow.positive?
77
+ end
78
+
79
+ def compute_cap
80
+ (3600 / 5).to_i # 720 samples — 1h at the 5s cadence
81
+ end
82
+ end
83
+ end
@@ -39,8 +39,8 @@ module SolidObserver
39
39
 
40
40
  widths = calculate_column_widths(headers, rows)
41
41
 
42
- output(format_table_row(headers, widths), color: :cyan)
43
- output(separator_line(widths), color: :cyan)
42
+ output(format_table_row(headers, widths), color: :red)
43
+ output(separator_line(widths), color: :red)
44
44
 
45
45
  rows.each do |row|
46
46
  output(format_table_row(row, widths))
@@ -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
 
@@ -5,23 +5,31 @@ module SolidObserver
5
5
  class Storage < Base
6
6
  def call
7
7
  print_section_header("💾 Storage Status")
8
+ return print_realtime_mode_message if SolidObserver.config.realtime_mode?
8
9
 
9
- current_stats = gather_storage_stats
10
+ render_storage_status
11
+ end
10
12
 
11
- if current_stats[:error]
12
- error(current_stats[:error])
13
- return
14
- end
13
+ private
14
+
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("")
19
+ end
20
+
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
15
25
 
16
26
  print_storage_table(current_stats)
17
27
  print_configuration
18
28
  end
19
29
 
20
- private
21
-
22
30
  def gather_storage_stats
23
31
  {
24
- db_size_bytes: calculate_database_size,
32
+ db_size_bytes: SolidObserver::Services::DatabaseSize.call(connection: QueueEvent.connection),
25
33
  event_count: QueueEvent.count,
26
34
  max_size_bytes: SolidObserver.config.max_db_size
27
35
  }
@@ -29,37 +37,38 @@ module SolidObserver
29
37
  {error: "Failed to gather storage stats: #{e.message}"}
30
38
  end
31
39
 
32
- def calculate_database_size
33
- db_config = QueueEvent.connection_db_config
34
- db_path = db_config.database
35
-
36
- return 0 unless File.exist?(db_path)
37
-
38
- File.size(db_path)
39
- rescue => e
40
- warning("Could not calculate database size: #{e.message}")
41
- 0
42
- end
43
-
44
40
  def print_storage_table(stats)
45
- size_mb = bytes_to_mb(stats[:db_size_bytes])
46
- percentage = calculate_percentage(stats[:db_size_bytes], stats[:max_size_bytes])
47
- status = status_indicator(percentage)
41
+ event_count = stats[:event_count]
42
+ db_size_bytes = stats[:db_size_bytes]
43
+ max_size_bytes = stats[:max_size_bytes]
48
44
 
49
45
  table(
50
46
  headers: ["Component", "Size", "Events", "Usage", "Status"],
51
- rows: [[
52
- "Queue",
53
- format_size(size_mb),
54
- format_number(stats[:event_count]),
55
- "#{percentage}%",
56
- status
57
- ]]
47
+ rows: [storage_row(event_count: event_count, db_size_bytes: db_size_bytes, max_size_bytes: max_size_bytes)]
58
48
  )
59
49
 
60
50
  output("")
61
51
  end
62
52
 
53
+ def storage_row(event_count:, db_size_bytes:, max_size_bytes:)
54
+ size, usage, status = storage_displays(db_size_bytes, max_size_bytes)
55
+
56
+ [
57
+ "Queue",
58
+ size,
59
+ format_number(event_count),
60
+ usage,
61
+ status
62
+ ]
63
+ end
64
+
65
+ def storage_displays(db_size_bytes, max_size_bytes)
66
+ return ["N/A", "N/A", "— Unknown"] unless db_size_bytes
67
+
68
+ percentage = calculate_percentage(db_size_bytes, max_size_bytes)
69
+ [format_size(bytes_to_mb(db_size_bytes)), "#{percentage}%", status_indicator(percentage)]
70
+ end
71
+
63
72
  def print_configuration
64
73
  retention_days = (SolidObserver.config.event_retention / 1.day).to_i
65
74
  max_size_mb = bytes_to_mb(SolidObserver.config.max_db_size)
@@ -105,8 +114,8 @@ module SolidObserver
105
114
 
106
115
  def print_section_header(title)
107
116
  output("")
108
- output(title, color: :cyan)
109
- output("=" * 50, color: :cyan)
117
+ output(title, color: :red)
118
+ output("=" * 50, color: :red)
110
119
  output("")
111
120
  end
112
121
  end
@@ -15,14 +15,13 @@ 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,
@@ -31,51 +30,58 @@ module SolidObserver
31
30
  # Retention Settings
32
31
  attr_accessor :event_retention
33
32
 
34
- # Retention Settings (planned for v0.2.0)
33
+ # Retention Settings (planned for a future release)
35
34
  # @note Metrics cleanup is not yet implemented
36
35
  attr_accessor :metrics_retention
37
36
 
38
37
  # Storage Settings
39
38
  attr_accessor :max_db_size
40
39
 
40
+ # Storage Mode
41
+ attr_reader :storage_mode
42
+
41
43
  # Performance Settings (with validation)
42
44
  attr_reader :sampling_rate,
43
45
  :warning_threshold,
44
46
  :buffer_size,
45
- :flush_interval
47
+ :flush_interval,
48
+ :max_buffer_size,
49
+ :buffer_overflow_strategy,
50
+ :filter_cache_ttl
46
51
 
47
52
  # Correlation Settings
48
53
  attr_accessor :correlation_id_generator
49
54
 
50
55
  def initialize
51
- # UI defaults
52
- @ui_enabled = !production?
53
- @ui_base_controller = "::ApplicationController"
54
- @http_basic_auth_enabled = false
55
- @http_basic_auth_user = nil
56
- @http_basic_auth_password = nil
57
-
58
- # Observer defaults
59
- @observe_queue = true
60
- @observe_cache = false
61
- @observe_cable = false
62
-
63
- # Retention defaults
64
- @event_retention = 30.days
65
- @metrics_retention = 90.days
66
-
67
- # Storage defaults
68
- @max_db_size = 1.gigabyte
69
- @warning_threshold = 0.8
70
-
71
- # Performance defaults
72
- @sampling_rate = 1.0
73
- @cache_sampling_rate = 0.1
74
- @buffer_size = 1000
75
- @flush_interval = 10.seconds
76
-
77
- # Correlation defaults
78
- @correlation_id_generator = nil
56
+ @ui_enabled, @ui_base_controller, @ui_username, @ui_password,
57
+ @storage_mode, @observe_queue, @observe_cache, @observe_cable,
58
+ @event_retention, @metrics_retention, @max_db_size, @warning_threshold,
59
+ @sampling_rate, @cache_sampling_rate, @buffer_size, @flush_interval,
60
+ @max_buffer_size, @buffer_overflow_strategy, @filter_cache_ttl,
61
+ @correlation_id_generator = !production?, "::ApplicationController", nil, nil,
62
+ :persistence, true, false, false,
63
+ 30.days, 90.days, 1.gigabyte, 0.8,
64
+ 1.0, 0.1, 1000, 10.seconds,
65
+ 10_000, :drop_old, 1.minute,
66
+ nil
67
+ end
68
+
69
+ STORAGE_MODES = %i[persistence realtime].freeze
70
+ BUFFER_OVERFLOW_STRATEGIES = %i[drop_old drop_new].freeze
71
+
72
+ def storage_mode=(value)
73
+ value = value.to_sym
74
+ raise ArgumentError, "storage_mode must be :persistence or :realtime" unless STORAGE_MODES.include?(value)
75
+
76
+ @storage_mode = value
77
+ end
78
+
79
+ def persistence_mode?
80
+ @storage_mode == :persistence
81
+ end
82
+
83
+ def realtime_mode?
84
+ @storage_mode == :realtime
79
85
  end
80
86
 
81
87
  def sampling_rate=(value)
@@ -90,6 +96,10 @@ module SolidObserver
90
96
 
91
97
  def buffer_size=(value)
92
98
  validate_positive_integer!(:buffer_size, value)
99
+ if defined?(@max_buffer_size) && value > @max_buffer_size
100
+ raise ArgumentError, "buffer_size must be <= max_buffer_size"
101
+ end
102
+
93
103
  @buffer_size = value
94
104
  end
95
105
 
@@ -98,6 +108,29 @@ module SolidObserver
98
108
  @flush_interval = value
99
109
  end
100
110
 
111
+ def max_buffer_size=(value)
112
+ validate_positive_integer!(:max_buffer_size, value)
113
+ if defined?(@buffer_size) && value < @buffer_size
114
+ raise ArgumentError, "max_buffer_size must be >= buffer_size"
115
+ end
116
+
117
+ @max_buffer_size = value
118
+ end
119
+
120
+ def buffer_overflow_strategy=(value)
121
+ value = value.to_sym
122
+ unless BUFFER_OVERFLOW_STRATEGIES.include?(value)
123
+ raise ArgumentError, "buffer_overflow_strategy must be :drop_old or :drop_new"
124
+ end
125
+
126
+ @buffer_overflow_strategy = value
127
+ end
128
+
129
+ def filter_cache_ttl=(value)
130
+ validate_positive_numeric!(:filter_cache_ttl, value)
131
+ @filter_cache_ttl = value
132
+ end
133
+
101
134
  private
102
135
 
103
136
  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
@@ -4,6 +4,10 @@ module SolidObserver
4
4
  class Engine < ::Rails::Engine
5
5
  isolate_namespace SolidObserver
6
6
 
7
+ middleware.use ActionDispatch::Cookies
8
+ middleware.use ActionDispatch::Session::CookieStore, key: "_solid_observer_session"
9
+ middleware.use ActionDispatch::Flash
10
+
7
11
  class << self
8
12
  def check_solid_queue_availability
9
13
  return if defined?(SolidQueue)
@@ -11,14 +15,35 @@ module SolidObserver
11
15
  Rails.logger.warn "[SolidObserver] SolidQueue not detected. Queue observability features will be limited."
12
16
  end
13
17
 
18
+ def check_ui_authentication
19
+ Services::UiAuthCheck.call(config: SolidObserver.config)
20
+ end
21
+
14
22
  def configure_database_connection
15
- db_config = ActiveRecord::Base.configurations.configs_for(
23
+ return if SolidObserver.config.realtime_mode?
24
+ return unless queue_db_config
25
+
26
+ connect_observer_models
27
+ end
28
+
29
+ def activate_subscribers
30
+ return activate_subscribers_in_realtime if SolidObserver.config.realtime_mode?
31
+ return if activation_skipped_for_table_status?
32
+
33
+ Rails.logger.info "[SolidObserver] Activating event subscribers"
34
+ Subscriber.subscribe!
35
+ end
36
+
37
+ private
38
+
39
+ def queue_db_config
40
+ ActiveRecord::Base.configurations.configs_for(
16
41
  env_name: Rails.env,
17
42
  name: "solid_observer_queue"
18
43
  )
44
+ end
19
45
 
20
- return unless db_config
21
-
46
+ def connect_observer_models
22
47
  connection_config = {
23
48
  database: {writing: :solid_observer_queue, reading: :solid_observer_queue}
24
49
  }
@@ -27,24 +52,58 @@ module SolidObserver
27
52
  SolidObserver::BaseMetric.connects_to(**connection_config)
28
53
  end
29
54
 
30
- def activate_subscribers
31
- logger = Rails.logger
55
+ def activate_subscribers_in_realtime
56
+ Rails.logger.info "[SolidObserver] Starting in real-time mode (no persistence)"
57
+ Subscriber.subscribe!
58
+ end
32
59
 
33
- unless table_exists?("solid_observer_queue_events")
34
- logger.info "[SolidObserver] Tables not found. Run: rails solid_observer:install:migrations && rails db:migrate"
35
- return
60
+ def activation_skipped_for_table_status?
61
+ case table_status("solid_observer_queue_events")
62
+ when :absent
63
+ log_activation_skip("Tables not found. Run: rails solid_observer:install:migrations && rails db:migrate")
64
+ true
65
+ when :unknown
66
+ log_activation_skip("Database not reachable at boot. Skipping subscriber activation.")
67
+ true
68
+ else
69
+ false
36
70
  end
71
+ end
37
72
 
38
- logger.info "[SolidObserver] Activating event subscribers"
39
- Subscriber.subscribe!
40
- rescue ActiveRecord::NoDatabaseError
41
- logger.info "[SolidObserver] Database not ready yet. Skipping subscriber activation."
73
+ def log_activation_skip(message)
74
+ Rails.logger.info("[SolidObserver] #{message}")
42
75
  end
43
76
 
44
- private
77
+ def table_status(table_name)
78
+ pool = SolidObserver::BaseEvent.connection_pool
79
+
80
+ return :present if cached_data_source_exists?(pool, table_name)
81
+
82
+ data_source_exists_in_db?(pool, table_name) ? :present : :absent
83
+ rescue *boot_connection_errors
84
+ :unknown
85
+ end
86
+
87
+ def cached_data_source_exists?(pool, table_name)
88
+ cache = pool.schema_cache
89
+ cache.data_source_exists?(pool, table_name)
90
+ rescue ArgumentError
91
+ cache.data_source_exists?(table_name)
92
+ end
93
+
94
+ def data_source_exists_in_db?(pool, table_name)
95
+ pool.with_connection { |connection| connection.data_source_exists?(table_name) }
96
+ end
45
97
 
46
- def table_exists?(table_name)
47
- ActiveRecord::Base.connection.table_exists?(table_name)
98
+ def boot_connection_errors
99
+ [
100
+ ActiveRecord::NoDatabaseError,
101
+ ActiveRecord::ConnectionNotEstablished,
102
+ ActiveRecord::StatementInvalid,
103
+ *([PG::ConnectionBad] if defined?(PG::ConnectionBad)),
104
+ *([Mysql2::Error::ConnectionError] if defined?(Mysql2::Error::ConnectionError)),
105
+ *([SQLite3::CantOpenException] if defined?(SQLite3::CantOpenException))
106
+ ]
48
107
  end
49
108
  end
50
109
 
@@ -55,6 +114,7 @@ module SolidObserver
55
114
  config.after_initialize do
56
115
  Engine.configure_database_connection
57
116
  Engine.activate_subscribers
117
+ Engine.check_ui_authentication
58
118
  end
59
119
  end
60
120
  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