solid_observer 0.4.0 → 0.6.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/README.md +100 -25
  4. data/app/assets/javascripts/solid_observer/live_poll.js +3 -1
  5. data/app/controllers/solid_observer/application_controller.rb +1 -0
  6. data/app/controllers/solid_observer/cable_dashboard_controller.rb +52 -0
  7. data/app/controllers/solid_observer/cable_operations_controller.rb +16 -0
  8. data/app/controllers/solid_observer/cache_dashboard_controller.rb +33 -40
  9. data/app/controllers/solid_observer/dashboard_controller.rb +21 -13
  10. data/app/controllers/solid_observer/traces_controller.rb +17 -0
  11. data/app/helpers/solid_observer/application_helper.rb +145 -3
  12. data/app/models/solid_observer/cable_event.rb +13 -0
  13. data/app/models/solid_observer/cable_metric.rb +12 -0
  14. data/app/models/solid_observer/cache_metric.rb +1 -2
  15. data/app/models/solid_observer/storage_info.rb +1 -1
  16. data/app/views/layouts/solid_observer/application.html.erb +35 -9
  17. data/app/views/solid_observer/cable_dashboard/_charts.html.erb +31 -0
  18. data/app/views/solid_observer/cable_dashboard/_recent_events.html.erb +39 -0
  19. data/app/views/solid_observer/cable_dashboard/_summary.html.erb +34 -0
  20. data/app/views/solid_observer/cable_dashboard/index.html.erb +118 -0
  21. data/app/views/solid_observer/cache_dashboard/_recent_events.html.erb +6 -1
  22. data/app/views/solid_observer/dashboard/_component_cards.html.erb +18 -0
  23. data/app/views/solid_observer/dashboard/_health.html.erb +6 -0
  24. data/app/views/solid_observer/dashboard/_home.html.erb +10 -0
  25. data/app/views/solid_observer/dashboard/_queue.html.erb +138 -0
  26. data/app/views/solid_observer/dashboard/_queue_table.html.erb +1 -0
  27. data/app/views/solid_observer/dashboard/_unified_feed.html.erb +38 -0
  28. data/app/views/solid_observer/dashboard/index.html.erb +3 -139
  29. data/app/views/solid_observer/events/index.html.erb +7 -1
  30. data/app/views/solid_observer/jobs/index.html.erb +1 -0
  31. data/app/views/solid_observer/storages/show.html.erb +29 -3
  32. data/app/views/solid_observer/traces/show.html.erb +80 -0
  33. data/config/routes.rb +3 -0
  34. data/db/migrate/20260612000001_add_event_type_recorded_at_index_to_cache_events.rb +21 -0
  35. data/db/migrate/20260619000001_create_solid_observer_cable_events.rb +22 -0
  36. data/db/migrate/20260619000002_create_solid_observer_cable_metrics.rb +17 -0
  37. data/db/migrate/20260630000001_add_correlation_id_to_cache_events.rb +8 -0
  38. data/db/migrate/20260630000002_add_correlation_id_to_cable_events.rb +8 -0
  39. data/lib/generators/solid_observer/install_generator.rb +8 -1
  40. data/lib/generators/solid_observer/templates/initializer.rb.tt +24 -5
  41. data/lib/solid_observer/base_event.rb +1 -1
  42. data/lib/solid_observer/base_metric.rb +1 -1
  43. data/lib/solid_observer/base_record.rb +8 -0
  44. data/lib/solid_observer/cable_event_buffer.rb +28 -0
  45. data/lib/solid_observer/cable_metric_buffer.rb +230 -0
  46. data/lib/solid_observer/cable_subscriber.rb +57 -0
  47. data/lib/solid_observer/cache_event_buffer.rb +11 -36
  48. data/lib/solid_observer/cache_metric_buffer.rb +229 -0
  49. data/lib/solid_observer/chart_buffer.rb +84 -27
  50. data/lib/solid_observer/cli/health.rb +49 -0
  51. data/lib/solid_observer/cli/trace.rb +106 -0
  52. data/lib/solid_observer/configuration.rb +47 -7
  53. data/lib/solid_observer/correlated.rb +23 -0
  54. data/lib/solid_observer/engine.rb +46 -28
  55. data/lib/solid_observer/event_buffer_core.rb +218 -0
  56. data/lib/solid_observer/queries/trace_query.rb +151 -0
  57. data/lib/solid_observer/queue_event_buffer.rb +9 -201
  58. data/lib/solid_observer/services/cable_operations.rb +74 -0
  59. data/lib/solid_observer/services/cable_stats.rb +385 -0
  60. data/lib/solid_observer/services/cache_stats.rb +35 -18
  61. data/lib/solid_observer/services/cleanup_storage.rb +82 -47
  62. data/lib/solid_observer/services/flush_cable_event_buffer.rb +54 -0
  63. data/lib/solid_observer/services/flush_cable_metrics.rb +54 -0
  64. data/lib/solid_observer/services/flush_cache_metrics.rb +56 -0
  65. data/lib/solid_observer/services/health_score.rb +85 -0
  66. data/lib/solid_observer/services/record_cable_event.rb +115 -0
  67. data/lib/solid_observer/services/record_cable_metric.rb +73 -0
  68. data/lib/solid_observer/services/record_cache_event.rb +24 -0
  69. data/lib/solid_observer/services/record_cache_metric.rb +13 -21
  70. data/lib/solid_observer/services/storage_info_snapshot.rb +103 -15
  71. data/lib/solid_observer/services/unified_feed.rb +101 -0
  72. data/lib/solid_observer/version.rb +1 -1
  73. data/lib/solid_observer.rb +42 -11
  74. data/lib/tasks/solid_observer.rake +97 -21
  75. metadata +41 -6
  76. data/app/assets/stylesheets/solid_observer/application.css +0 -18
  77. data/bin/console +0 -11
  78. data/bin/quality_gate +0 -95
  79. data/bin/setup +0 -8
@@ -2,7 +2,40 @@
2
2
 
3
3
  module SolidObserver
4
4
  class ChartBuffer
5
+ CACHE_KEY = "solid_observer/chart_buffer/ready_samples"
5
6
  INSTANCE_MUTEX = Mutex.new
7
+ SAMPLE_CAP = 720
8
+ STORAGE_MUTEX = Mutex.new
9
+
10
+ class SampleWindow
11
+ def initialize(samples)
12
+ @samples = samples
13
+ end
14
+
15
+ def upsert(sample, cap:)
16
+ return replace_latest_sample(sample) if latest_sample_timestamp == sample[:t]
17
+
18
+ append_new_sample(sample, cap: cap)
19
+ end
20
+
21
+ private
22
+
23
+ def latest_sample_timestamp
24
+ @samples.last&.[](:t)
25
+ end
26
+
27
+ def replace_latest_sample(sample)
28
+ @samples[-1] = sample
29
+ @samples
30
+ end
31
+
32
+ def append_new_sample(sample, cap:)
33
+ @samples << sample
34
+ overflow = @samples.length - cap
35
+ @samples.shift(overflow) if overflow.positive?
36
+ @samples
37
+ end
38
+ end
6
39
 
7
40
  class << self
8
41
  def append(value, at: Time.now)
@@ -24,16 +57,12 @@ module SolidObserver
24
57
  end
25
58
  end
26
59
 
27
- def initialize
28
- @mutex = Mutex.new
29
- @samples = []
30
- @cap = nil
31
- end
60
+ @fallback_samples = []
32
61
 
33
62
  def append(value, at: Time.now)
34
63
  sample = {t: at.to_i, v: value.to_i}
35
64
 
36
- @mutex.synchronize { store_sample(sample) }
65
+ STORAGE_MUTEX.synchronize { persist_sample(sample) }
37
66
 
38
67
  sample
39
68
  end
@@ -41,43 +70,71 @@ module SolidObserver
41
70
  def recent(window_seconds)
42
71
  cutoff = Time.now.to_i - window_seconds.to_i
43
72
 
44
- @mutex.synchronize do
45
- @samples.select { |sample| sample[:t] >= cutoff }.map(&:dup)
73
+ STORAGE_MUTEX.synchronize do
74
+ load_samples.select { |sample| sample[:t] >= cutoff }.map(&:dup)
46
75
  end
47
76
  end
48
77
 
49
78
  def clear
50
- @mutex.synchronize do
51
- @samples.clear
52
- @cap = nil
79
+ STORAGE_MUTEX.synchronize do
80
+ empty_samples = []
81
+ replace_fallback_samples(empty_samples)
82
+ cache_store&.delete(CACHE_KEY)
83
+ rescue
84
+ nil
53
85
  end
54
86
  end
55
87
 
56
88
  private
57
89
 
58
- def store_sample(sample)
59
- @cap ||= compute_cap
60
- replace_or_append(sample)
61
- trim_to_cap
90
+ def persist_sample(sample)
91
+ samples = SampleWindow.new(load_samples).upsert(sample, cap: SAMPLE_CAP)
92
+ write_samples(samples)
62
93
  end
63
94
 
64
- def replace_or_append(sample)
65
- latest_sample = @samples.last
95
+ def load_samples
96
+ normalize_samples(cache_store&.read(CACHE_KEY) || stored_fallback_samples)
97
+ rescue
98
+ stored_fallback_samples
99
+ end
66
100
 
67
- if latest_sample && latest_sample[:t] == sample[:t]
68
- @samples[-1] = sample
69
- else
70
- @samples << sample
71
- end
101
+ def write_samples(samples)
102
+ normalized = normalize_samples(samples)
103
+
104
+ replace_fallback_samples(normalized.map(&:dup))
105
+ cache_store&.write(CACHE_KEY, normalized)
106
+ rescue
107
+ replace_fallback_samples(normalized)
72
108
  end
73
109
 
74
- def trim_to_cap
75
- overflow = @samples.length - @cap
76
- @samples.shift(overflow) if overflow.positive?
110
+ def cache_store
111
+ return unless defined?(Rails)
112
+
113
+ Rails.cache
77
114
  end
78
115
 
79
- def compute_cap
80
- (3600 / 5).to_i # 720 samples — 1h at the 5s cadence
116
+ def stored_fallback_samples
117
+ normalize_samples(self.class.instance_variable_get(:@fallback_samples))
118
+ end
119
+
120
+ def replace_fallback_samples(samples)
121
+ self.class.instance_variable_set(:@fallback_samples, samples)
122
+ end
123
+
124
+ def normalize_samples(samples)
125
+ Array(samples).filter_map do |sample|
126
+ normalize_sample(sample)
127
+ end.last(SAMPLE_CAP)
128
+ end
129
+
130
+ def normalize_sample(sample)
131
+ return unless sample.is_a?(Hash)
132
+
133
+ timestamp = sample[:t] || sample["t"]
134
+ value = sample[:v] || sample["v"]
135
+ return unless timestamp && value
136
+
137
+ {t: timestamp.to_i, v: value.to_i}
81
138
  end
82
139
  end
83
140
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidObserver
4
+ module CLI
5
+ class Health < Base
6
+ STATUS_COLORS = {stable: :green, degraded: :yellow, critical: :red}.freeze
7
+ NAME_WIDTH = 8
8
+ UNAVAILABLE_SUFFIX = " (unavailable)"
9
+
10
+ def call
11
+ health = SolidObserver::Services::HealthScore.call
12
+
13
+ print_header
14
+ print_overall(health[:overall])
15
+ print_components(health[:components])
16
+ output("")
17
+ end
18
+
19
+ private
20
+
21
+ def print_header
22
+ output("\n🩺 SolidObserver Health", color: :red)
23
+ output("=" * 50, color: :red)
24
+ output("")
25
+ end
26
+
27
+ def print_overall(overall)
28
+ output("Overall: #{overall}", color: STATUS_COLORS[overall])
29
+ output("")
30
+ end
31
+
32
+ def print_components(components)
33
+ return warning("⚠️ No components enabled") if components.empty?
34
+
35
+ output("Components", color: :green)
36
+ output("")
37
+ components.each { |name, entry| print_component(name, entry[:status], entry[:available]) }
38
+ end
39
+
40
+ # Renders the status verbatim: HealthScore owns the worst-of ladder, so an
41
+ # unrecognised status stays uncoloured rather than being coerced to stable.
42
+ # :reek:ControlParameter
43
+ def print_component(name, status, available)
44
+ suffix = available ? "" : UNAVAILABLE_SUFFIX
45
+ output(" #{name.to_s.ljust(NAME_WIDTH)}#{status}#{suffix}", color: STATUS_COLORS[status])
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidObserver
4
+ module CLI
5
+ class Trace < Base
6
+ DETAIL_FORMATTERS = {
7
+ queue: :format_queue_details,
8
+ cache: :format_cache_details,
9
+ cable: :format_cable_details
10
+ }.freeze
11
+
12
+ EVENT_FORMATTERS = {
13
+ "cable_summary" => ->(row) { "and #{row[:collapsed_count]} more cable events" }
14
+ }.freeze
15
+
16
+ def call(correlation_id:, limit: nil)
17
+ return output("Trace unavailable in realtime mode.") if SolidObserver.config.realtime_mode?
18
+
19
+ result = SolidObserver::Queries::TraceQuery.new.call(correlation_id: correlation_id, limit: limit)
20
+ return warning("No events found for correlation_id: #{correlation_id}") if (rows = result.rows).empty?
21
+
22
+ print_trace_table(rows)
23
+ print_unavailable_components(result.unavailable_components)
24
+ end
25
+
26
+ private
27
+
28
+ def print_trace_table(rows)
29
+ print_section_header("Trace")
30
+ table(
31
+ headers: ["Time", "Component", "Event", "Details"],
32
+ rows: rows.map { |row| format_cells(row) }
33
+ )
34
+ output("")
35
+ end
36
+
37
+ def print_unavailable_components(unavailable)
38
+ unavailable.each { |component| warning("#{component} unavailable") }
39
+ end
40
+
41
+ def format_cells(row)
42
+ recorded_at, component, event_type = row.values_at(:recorded_at, :component, :event_type)
43
+ [
44
+ format_time(recorded_at),
45
+ component.to_s,
46
+ event_type,
47
+ format_details(row)
48
+ ]
49
+ end
50
+
51
+ def format_details(row)
52
+ event_type, component = row.values_at(:event_type, :component)
53
+ event_formatter = EVENT_FORMATTERS[event_type]
54
+ return event_formatter.call(row) if event_formatter
55
+
56
+ component_formatter = DETAIL_FORMATTERS[component]
57
+ send(component_formatter, row) if component_formatter
58
+ end
59
+
60
+ def format_queue_details(row)
61
+ job_class, queue_name, duration = row.values_at(:job_class, :queue_name, :duration)
62
+ [label("job", job_class), label("queue", queue_name), duration_part(duration)].compact.join(" ")
63
+ end
64
+
65
+ def format_cache_details(row)
66
+ hit, error_class, duration = row.values_at(:hit, :error_class, :duration)
67
+ [hit_part(hit), error_part(error_class), duration_part(duration)].compact.join(" ")
68
+ end
69
+
70
+ def format_cable_details(row)
71
+ channel_class, duration, error_class, collapsed_count = row.values_at(:channel_class, :duration, :error_class, :collapsed_count)
72
+ [label("channel", channel_class), error_part(error_class), duration_part(duration), ("collapsed=#{collapsed_count}" if collapsed_count)].compact.join(" ")
73
+ end
74
+
75
+ def label(name, value)
76
+ "#{name}=#{value}" if value
77
+ end
78
+
79
+ def error_part(error_class)
80
+ "error=#{error_class}" if error_class
81
+ end
82
+
83
+ def hit_part(hit)
84
+ "hit=#{hit}" if [true, false].include?(hit)
85
+ end
86
+
87
+ def duration_part(duration)
88
+ "duration=#{format_duration(duration)}" if duration
89
+ end
90
+
91
+ def format_duration(duration)
92
+ duration ? "#{duration.round(3)}s" : nil
93
+ end
94
+
95
+ def format_time(time)
96
+ time.strftime("%Y-%m-%d %H:%M:%S")
97
+ end
98
+
99
+ def print_section_header(title)
100
+ output("\n#{title}", color: :red)
101
+ output("=" * 50, color: :red)
102
+ output("")
103
+ end
104
+ end
105
+ end
106
+ end
@@ -21,19 +21,20 @@ module SolidObserver
21
21
  # Observer Settings
22
22
  attr_accessor :observe_queue
23
23
 
24
- # Observer Settings (planned for a future release)
25
- # @note Cache and Cable observers are not yet implemented
24
+ # Cache & Cable Observer Settings
26
25
  attr_accessor :observe_cache,
27
26
  :observe_cable,
28
27
  :cache_sampling_rate,
29
28
  :cache_slow_threshold,
30
29
  :cache_store_errors
31
30
 
31
+ attr_reader :cable_rejection_threshold,
32
+ :cable_backlog_threshold,
33
+ :cable_error_threshold
34
+
32
35
  # Retention Settings
33
36
  attr_accessor :event_retention
34
37
 
35
- # Retention Settings (planned for a future release)
36
- # @note Metrics cleanup is not yet implemented
37
38
  attr_accessor :metrics_retention
38
39
 
39
40
  # Storage Settings
@@ -49,7 +50,8 @@ module SolidObserver
49
50
  :flush_interval,
50
51
  :max_buffer_size,
51
52
  :buffer_overflow_strategy,
52
- :filter_cache_ttl
53
+ :filter_cache_ttl,
54
+ :cable_sampling_rate
53
55
 
54
56
  # Correlation Settings
55
57
  attr_accessor :correlation_id_generator
@@ -58,13 +60,16 @@ module SolidObserver
58
60
  @ui_enabled, @ui_base_controller, @ui_username, @ui_password,
59
61
  @storage_mode, @observe_queue, @observe_cache, @observe_cable,
60
62
  @event_retention, @metrics_retention, @max_db_size, @warning_threshold,
61
- @sampling_rate, @cache_sampling_rate, @cache_slow_threshold, @cache_store_errors,
63
+ @sampling_rate, @cache_sampling_rate, @cable_sampling_rate, @cache_slow_threshold, @cache_store_errors,
64
+ @cable_rejection_threshold, @cable_backlog_threshold, @cable_error_threshold,
62
65
  @buffer_size, @flush_interval,
63
66
  @max_buffer_size, @buffer_overflow_strategy, @filter_cache_ttl,
64
67
  @correlation_id_generator = !production?, "::ApplicationController", nil, nil,
65
68
  :persistence, true, false, false,
66
69
  30.days, 90.days, 1.gigabyte, 0.8,
67
- 1.0, 0.1, 0.1, true, 1000, 10.seconds,
70
+ 1.0, 0.1, 0.1, 0.1, true,
71
+ 0.05, 0.10, 0.0,
72
+ 1000, 10.seconds,
68
73
  10_000, :drop_old, 1.minute,
69
74
  nil
70
75
  end
@@ -95,6 +100,10 @@ module SolidObserver
95
100
  !!defined?(::SolidCache)
96
101
  end
97
102
 
103
+ def solid_cable_available?
104
+ !!defined?(::SolidCable)
105
+ end
106
+
98
107
  def solid_queue_enabled?
99
108
  observe_queue
100
109
  end
@@ -103,11 +112,35 @@ module SolidObserver
103
112
  observe_cache && solid_cache_available?
104
113
  end
105
114
 
115
+ def solid_cable_enabled?
116
+ observe_cable && solid_cable_available?
117
+ end
118
+
106
119
  def sampling_rate=(value)
107
120
  validate_rate!(:sampling_rate, value)
108
121
  @sampling_rate = value
109
122
  end
110
123
 
124
+ def cable_sampling_rate=(value)
125
+ validate_rate!(:cable_sampling_rate, value)
126
+ @cable_sampling_rate = value
127
+ end
128
+
129
+ def cable_rejection_threshold=(value)
130
+ validate_rate!(:cable_rejection_threshold, value)
131
+ @cable_rejection_threshold = value
132
+ end
133
+
134
+ def cable_backlog_threshold=(value)
135
+ validate_rate!(:cable_backlog_threshold, value)
136
+ @cable_backlog_threshold = value
137
+ end
138
+
139
+ def cable_error_threshold=(value)
140
+ validate_non_negative_numeric!(:cable_error_threshold, value)
141
+ @cable_error_threshold = value
142
+ end
143
+
111
144
  def warning_threshold=(value)
112
145
  validate_rate!(:warning_threshold, value)
113
146
  @warning_threshold = value
@@ -170,6 +203,13 @@ module SolidObserver
170
203
  end
171
204
  end
172
205
 
206
+ # :reek:FeatureEnvy
207
+ def validate_non_negative_numeric!(name, value)
208
+ unless value.is_a?(Numeric) && value >= 0
209
+ raise ArgumentError, "#{name} must be a non-negative number"
210
+ end
211
+ end
212
+
173
213
  def production?
174
214
  defined?(Rails) && Rails.env.production?
175
215
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidObserver
4
+ module Correlated
5
+ def self.included(base)
6
+ base.around_perform do |job, block|
7
+ SolidObserver::Correlated.stamp(job.job_id) { block.call }
8
+ end
9
+ end
10
+
11
+ def self.stamp(correlation_id)
12
+ thread_local = Thread.current
13
+ previous = thread_local[:solid_observer_correlation_id]
14
+ thread_local[:solid_observer_correlation_id] = correlation_id if correlation_id.present?
15
+
16
+ yield
17
+ ensure
18
+ thread_local[:solid_observer_correlation_id] = previous
19
+ end
20
+ end
21
+ end
22
+
23
+ ActiveSupport.on_load(:active_job) { include SolidObserver::Correlated }
@@ -1,20 +1,9 @@
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
-
11
3
  module SolidObserver
12
4
  class Engine < ::Rails::Engine
13
5
  isolate_namespace SolidObserver
14
6
 
15
- SOLID_QUEUE_AVAILABLE = defined?(::SolidQueue)
16
- SOLID_CACHE_AVAILABLE = defined?(::SolidCache)
17
-
18
7
  middleware.use ActionDispatch::Cookies
19
8
  middleware.use ActionDispatch::Session::CookieStore, key: "_solid_observer_session"
20
9
  middleware.use ActionDispatch::Flash
@@ -33,6 +22,13 @@ module SolidObserver
33
22
  Rails.logger.warn "[SolidObserver] SolidCache not detected. Cache observability features will be disabled."
34
23
  end
35
24
 
25
+ def check_solid_cable_availability
26
+ return if defined?(::SolidCable)
27
+ return unless SolidObserver.config.observe_cable
28
+
29
+ Rails.logger.warn "[SolidObserver] SolidCable not detected. Cable observability features will be disabled."
30
+ end
31
+
36
32
  def check_ui_authentication
37
33
  Services::UiAuthCheck.call(config: SolidObserver.config)
38
34
  end
@@ -46,16 +42,18 @@ module SolidObserver
46
42
 
47
43
  def activate_subscribers
48
44
  return activate_subscribers_in_realtime if SolidObserver.config.realtime_mode?
49
- return if activation_skipped_for_table_status_for_enabled_components?
50
45
 
51
46
  Rails.logger.info "[SolidObserver] Activating event subscribers"
52
- Subscriber.subscribe! if should_activate_queue_subscriber?
53
- CacheSubscriber.subscribe! if should_activate_cache_subscriber?
47
+ activate_queue_subscriber
48
+ activate_cache_subscriber
49
+ activate_cable_subscriber
54
50
  end
55
51
 
56
52
  private
57
53
 
58
54
  def queue_db_config
55
+ return unless active_record_available?
56
+
59
57
  ActiveRecord::Base.configurations.configs_for(
60
58
  env_name: Rails.env,
61
59
  name: "solid_observer_queue"
@@ -67,34 +65,40 @@ module SolidObserver
67
65
  database: {writing: :solid_observer_queue, reading: :solid_observer_queue}
68
66
  }
69
67
 
70
- SolidObserver::BaseEvent.connects_to(**connection_config)
71
- SolidObserver::BaseMetric.connects_to(**connection_config)
68
+ SolidObserver::BaseRecord.connects_to(**connection_config)
69
+ end
70
+
71
+ def active_record_available?
72
+ defined?(::ActiveRecord::Base)
72
73
  end
73
74
 
74
75
  def activate_subscribers_in_realtime
75
76
  Rails.logger.info "[SolidObserver] Starting in real-time mode (no persistence)"
76
77
  Subscriber.subscribe! if should_activate_queue_subscriber?
77
78
  CacheSubscriber.subscribe! if should_activate_cache_subscriber?
79
+ CableSubscriber.subscribe! if should_activate_cable_subscriber?
78
80
  end
79
81
 
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?
82
+ def activate_queue_subscriber
83
+ activate_subscriber_for_table("solid_observer_queue_events", Subscriber) if should_activate_queue_subscriber?
84
+ end
84
85
 
85
- enabled_tables.any? { |table_name| skip_activation_for_missing_table?(table_name) }
86
+ def activate_cache_subscriber
87
+ activate_subscriber_for_table("solid_observer_cache_events", CacheSubscriber) if should_activate_cache_subscriber?
86
88
  end
87
89
 
88
- def skip_activation_for_missing_table?(table_name)
90
+ def activate_cable_subscriber
91
+ activate_subscriber_for_table("solid_observer_cable_events", CableSubscriber) if should_activate_cable_subscriber?
92
+ end
93
+
94
+ def activate_subscriber_for_table(table_name, subscriber)
89
95
  case table_status(table_name)
90
96
  when :absent
91
97
  log_activation_skip("Tables not found (missing: #{table_name}). Run: rails solid_observer:install:migrations && rails db:migrate")
92
- true
93
98
  when :unknown
94
99
  log_activation_skip("Database not reachable at boot. Skipping subscriber activation.")
95
- true
96
100
  else
97
- false
101
+ subscriber.subscribe!
98
102
  end
99
103
  end
100
104
 
@@ -106,18 +110,28 @@ module SolidObserver
106
110
  SolidObserver.config.solid_cache_enabled?
107
111
  end
108
112
 
113
+ def should_activate_cable_subscriber?
114
+ SolidObserver.config.solid_cable_enabled?
115
+ end
116
+
109
117
  def log_activation_skip(message)
110
118
  Rails.logger.info("[SolidObserver] #{message}")
111
119
  end
112
120
 
113
121
  def table_status(table_name)
114
- pool = SolidObserver::BaseEvent.connection_pool
122
+ return :unknown unless active_record_available?
123
+
124
+ data_source_status(table_name)
125
+ rescue *boot_connection_errors
126
+ :unknown
127
+ end
128
+
129
+ def data_source_status(table_name)
130
+ pool = SolidObserver::BaseRecord.connection_pool
115
131
 
116
132
  return :present if cached_data_source_exists?(pool, table_name)
117
133
 
118
134
  data_source_exists_in_db?(pool, table_name) ? :present : :absent
119
- rescue *boot_connection_errors
120
- :unknown
121
135
  end
122
136
 
123
137
  def cached_data_source_exists?(pool, table_name)
@@ -132,12 +146,15 @@ module SolidObserver
132
146
  end
133
147
 
134
148
  def boot_connection_errors
149
+ return [] unless active_record_available?
150
+
135
151
  [
136
152
  ActiveRecord::NoDatabaseError,
137
153
  ActiveRecord::ConnectionNotEstablished,
138
154
  ActiveRecord::StatementInvalid,
139
155
  *([PG::ConnectionBad] if defined?(PG::ConnectionBad)),
140
156
  *([Mysql2::Error::ConnectionError] if defined?(Mysql2::Error::ConnectionError)),
157
+ *([Trilogy::Error] if defined?(Trilogy::Error)),
141
158
  *([SQLite3::CantOpenException] if defined?(SQLite3::CantOpenException))
142
159
  ]
143
160
  end
@@ -146,6 +163,7 @@ module SolidObserver
146
163
  config.before_initialize do
147
164
  Engine.check_solid_queue_availability
148
165
  Engine.check_solid_cache_availability
166
+ Engine.check_solid_cable_availability
149
167
  end
150
168
 
151
169
  config.after_initialize do