solid_observer 0.1.1 → 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 +63 -0
  3. data/README.md +157 -28
  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 -40
  45. data/lib/solid_observer/configuration.rb +47 -37
  46. data/lib/solid_observer/correlation_id_resolver.rb +8 -6
  47. data/lib/solid_observer/engine.rb +72 -17
  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 -25
  54. data/lib/solid_observer/queue_stats.rb +165 -19
  55. data/lib/solid_observer/services/cleanup_storage.rb +58 -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 +51 -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
 
@@ -4,32 +4,32 @@ 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
10
+ render_storage_status
11
+ end
18
12
 
19
- if current_stats[:error]
20
- error(current_stats[:error])
21
- return
22
- 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
23
25
 
24
26
  print_storage_table(current_stats)
25
27
  print_configuration
26
28
  end
27
29
 
28
- private
29
-
30
30
  def gather_storage_stats
31
31
  {
32
- db_size_bytes: calculate_database_size,
32
+ db_size_bytes: SolidObserver::Services::DatabaseSize.call(connection: QueueEvent.connection),
33
33
  event_count: QueueEvent.count,
34
34
  max_size_bytes: SolidObserver.config.max_db_size
35
35
  }
@@ -37,37 +37,38 @@ module SolidObserver
37
37
  {error: "Failed to gather storage stats: #{e.message}"}
38
38
  end
39
39
 
40
- def calculate_database_size
41
- db_config = QueueEvent.connection_db_config
42
- db_path = db_config.database
43
-
44
- return 0 unless File.exist?(db_path)
45
-
46
- File.size(db_path)
47
- rescue => e
48
- warning("Could not calculate database size: #{e.message}")
49
- 0
50
- end
51
-
52
40
  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)
41
+ event_count = stats[:event_count]
42
+ db_size_bytes = stats[:db_size_bytes]
43
+ max_size_bytes = stats[:max_size_bytes]
56
44
 
57
45
  table(
58
46
  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
- ]]
47
+ rows: [storage_row(event_count: event_count, db_size_bytes: db_size_bytes, max_size_bytes: max_size_bytes)]
66
48
  )
67
49
 
68
50
  output("")
69
51
  end
70
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
+
71
72
  def print_configuration
72
73
  retention_days = (SolidObserver.config.event_retention / 1.day).to_i
73
74
  max_size_mb = bytes_to_mb(SolidObserver.config.max_db_size)
@@ -113,8 +114,8 @@ module SolidObserver
113
114
 
114
115
  def print_section_header(title)
115
116
  output("")
116
- output(title, color: :cyan)
117
- output("=" * 50, color: :cyan)
117
+ output(title, color: :red)
118
+ output("=" * 50, color: :red)
118
119
  output("")
119
120
  end
120
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,7 +30,7 @@ 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
 
@@ -45,46 +44,30 @@ module SolidObserver
45
44
  attr_reader :sampling_rate,
46
45
  :warning_threshold,
47
46
  :buffer_size,
48
- :flush_interval
47
+ :flush_interval,
48
+ :max_buffer_size,
49
+ :buffer_overflow_strategy,
50
+ :filter_cache_ttl
49
51
 
50
52
  # Correlation Settings
51
53
  attr_accessor :correlation_id_generator
52
54
 
53
55
  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
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
85
67
  end
86
68
 
87
69
  STORAGE_MODES = %i[persistence realtime].freeze
70
+ BUFFER_OVERFLOW_STRATEGIES = %i[drop_old drop_new].freeze
88
71
 
89
72
  def storage_mode=(value)
90
73
  value = value.to_sym
@@ -113,6 +96,10 @@ module SolidObserver
113
96
 
114
97
  def buffer_size=(value)
115
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
+
116
103
  @buffer_size = value
117
104
  end
118
105
 
@@ -121,6 +108,29 @@ module SolidObserver
121
108
  @flush_interval = value
122
109
  end
123
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
+
124
134
  private
125
135
 
126
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,16 +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
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?
16
32
 
17
- db_config = ActiveRecord::Base.configurations.configs_for(
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(
18
41
  env_name: Rails.env,
19
42
  name: "solid_observer_queue"
20
43
  )
44
+ end
21
45
 
22
- return unless db_config
23
-
46
+ def connect_observer_models
24
47
  connection_config = {
25
48
  database: {writing: :solid_observer_queue, reading: :solid_observer_queue}
26
49
  }
@@ -29,27 +52,58 @@ module SolidObserver
29
52
  SolidObserver::BaseMetric.connects_to(**connection_config)
30
53
  end
31
54
 
32
- def activate_subscribers
33
- 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
34
59
 
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
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
40
68
  else
41
- logger.info "[SolidObserver] Activating event subscribers"
69
+ false
42
70
  end
71
+ end
43
72
 
44
- Subscriber.subscribe!
45
- rescue ActiveRecord::NoDatabaseError
46
- logger.info "[SolidObserver] Database not ready yet. Skipping subscriber activation."
73
+ def log_activation_skip(message)
74
+ Rails.logger.info("[SolidObserver] #{message}")
47
75
  end
48
76
 
49
- 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
50
97
 
51
- def table_exists?(table_name)
52
- 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
+ ]
53
107
  end
54
108
  end
55
109
 
@@ -60,6 +114,7 @@ module SolidObserver
60
114
  config.after_initialize do
61
115
  Engine.configure_database_connection
62
116
  Engine.activate_subscribers
117
+ Engine.check_ui_authentication
63
118
  end
64
119
  end
65
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
@@ -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