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
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/timer_task"
4
+
5
+ module SolidObserver
6
+ module EventBufferCore
7
+ INITIAL_METRICS = {
8
+ flush_failures_count: 0,
9
+ drops_count: 0,
10
+ last_flush_at: nil,
11
+ last_flush_duration_ms: nil,
12
+ last_flush_error: nil
13
+ }.freeze
14
+
15
+ def initialize_event_buffer
16
+ @mutex = Mutex.new
17
+ @metrics_mutex = Mutex.new
18
+ @buffer = []
19
+ @metrics = INITIAL_METRICS.dup
20
+ @timer_task = nil
21
+ end
22
+ private :initialize_event_buffer
23
+
24
+ def push(event_data)
25
+ return unless (config = SolidObserver.config).persistence_mode?
26
+
27
+ drops_count, should_flush = sync_push_and_check(event_data, config)
28
+ record_drop(drops_count) if drops_count.positive?
29
+ ensure_timer_running
30
+ flush! if should_flush
31
+ end
32
+
33
+ def flush!
34
+ events_to_flush = drain_buffer
35
+ return if events_to_flush.empty?
36
+
37
+ flush_events(events_to_flush, monotonic_ms)
38
+ end
39
+
40
+ def flush
41
+ flush!
42
+ end
43
+
44
+ def size
45
+ @mutex.synchronize { @buffer.size }
46
+ end
47
+
48
+ def clear
49
+ @mutex.synchronize { @buffer.clear }
50
+ end
51
+
52
+ def metrics
53
+ current_size = @mutex.synchronize { @buffer.size }
54
+ snapshot = @metrics_mutex.synchronize { @metrics.dup }
55
+ {
56
+ size: current_size,
57
+ max_buffer_size: SolidObserver.config.max_buffer_size
58
+ }.merge(snapshot)
59
+ end
60
+
61
+ def shutdown
62
+ stop_timer
63
+ flush!
64
+ end
65
+
66
+ private
67
+
68
+ def drain_buffer
69
+ @mutex.synchronize do
70
+ events_to_flush = @buffer.dup
71
+ @buffer.clear
72
+ events_to_flush
73
+ end
74
+ end
75
+
76
+ def flush_events(events_to_flush, started_at_ms)
77
+ flush_service.call(events_to_flush)
78
+ record_flush_success(monotonic_ms - started_at_ms)
79
+ rescue => error
80
+ handle_flush_error(error, events_to_flush)
81
+ end
82
+
83
+ def handle_flush_error(error, events_to_flush)
84
+ requeue_failed_events(events_to_flush)
85
+ record_flush_failure(error)
86
+ log_flush_failure(error)
87
+ nil
88
+ end
89
+
90
+ def log_flush_failure(error)
91
+ Rails.logger&.error("[SolidObserver] #{log_label} flush failed: #{error.message}") if defined?(Rails)
92
+ end
93
+
94
+ def apply_overflow_policy(event_data, config)
95
+ if @buffer.size < config.max_buffer_size
96
+ @buffer << event_data
97
+ return 0
98
+ end
99
+
100
+ handle_overflow(event_data, config.buffer_overflow_strategy)
101
+ end
102
+
103
+ def handle_overflow(event_data, overflow_strategy)
104
+ case overflow_strategy
105
+ when :drop_old
106
+ @buffer.shift
107
+ @buffer << event_data
108
+ when :drop_new
109
+ nil
110
+ else
111
+ raise ArgumentError, "Unsupported buffer_overflow_strategy: #{overflow_strategy.inspect}"
112
+ end
113
+
114
+ 1
115
+ end
116
+
117
+ def requeue_failed_events(events_to_flush)
118
+ dropped_count = sync_requeue_events(events_to_flush, SolidObserver.config.max_buffer_size)
119
+ record_drop(dropped_count) if dropped_count.positive?
120
+ end
121
+
122
+ def trim_events_for_capacity(events, max_buffer_size)
123
+ dropped_count = events.size - max_buffer_size
124
+ return [events, 0] if dropped_count <= 0
125
+
126
+ [events_to_keep(events, max_buffer_size), dropped_count]
127
+ end
128
+
129
+ def ensure_timer_running
130
+ timer_to_start, timer_to_stop = replace_timer_if_stopped
131
+ return unless timer_to_start
132
+
133
+ timer_to_stop&.shutdown
134
+ timer_to_start.execute
135
+ end
136
+
137
+ def replace_timer_if_stopped
138
+ @mutex.synchronize do
139
+ current_timer_task = @timer_task
140
+ return [nil, nil] if timer_running?(current_timer_task)
141
+
142
+ [build_timer_task, current_timer_task]
143
+ end
144
+ end
145
+
146
+ def stop_timer
147
+ timer_to_stop = @mutex.synchronize do
148
+ current_timer = @timer_task
149
+ @timer_task = nil
150
+ current_timer
151
+ end
152
+
153
+ timer_to_stop&.shutdown
154
+ end
155
+
156
+ def record_flush_success(duration_ms)
157
+ @metrics_mutex.synchronize do
158
+ @metrics.merge!(
159
+ last_flush_at: Time.current,
160
+ last_flush_duration_ms: duration_ms,
161
+ last_flush_error: nil
162
+ )
163
+ end
164
+ end
165
+
166
+ def record_flush_failure(error)
167
+ @metrics_mutex.synchronize do
168
+ @metrics[:flush_failures_count] += 1
169
+ @metrics[:last_flush_error] = error.message
170
+ end
171
+ end
172
+
173
+ def record_drop(count = 1)
174
+ @metrics_mutex.synchronize do
175
+ @metrics[:drops_count] += count
176
+ end
177
+ end
178
+
179
+ def sync_push_and_check(event_data, config)
180
+ @mutex.synchronize do
181
+ drops_count = apply_overflow_policy(event_data, config)
182
+ [drops_count, @buffer.size >= config.buffer_size]
183
+ end
184
+ end
185
+
186
+ def sync_requeue_events(events_to_flush, max_buffer_size)
187
+ @mutex.synchronize do
188
+ combined_events = events_to_flush + @buffer
189
+ kept_events, dropped_count = trim_events_for_capacity(combined_events, max_buffer_size)
190
+ @buffer.replace(kept_events)
191
+ dropped_count
192
+ end
193
+ end
194
+
195
+ def events_to_keep(events, max_buffer_size)
196
+ if SolidObserver.config.buffer_overflow_strategy == :drop_old
197
+ events.last(max_buffer_size)
198
+ else
199
+ events.first(max_buffer_size)
200
+ end
201
+ end
202
+
203
+ def timer_running?(timer_task)
204
+ timer_task && !timer_task.shuttingdown?
205
+ end
206
+
207
+ def build_timer_task
208
+ @timer_task = Concurrent::TimerTask.new(
209
+ execution_interval: SolidObserver.config.flush_interval,
210
+ run_now: false
211
+ ) { flush! }
212
+ end
213
+
214
+ def monotonic_ms
215
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidObserver
4
+ module Queries
5
+ class TraceQuery
6
+ DEFAULT_LIMIT = 100
7
+ CABLE_CAP = 50
8
+
9
+ Result = Struct.new(:rows, :unavailable_components, keyword_init: true)
10
+
11
+ COMPONENT_PRIORITY = {queue: 0, cache: 1, cable: 2}.freeze
12
+
13
+ COMPONENT_CONFIG = {
14
+ queue: {
15
+ enabled: :solid_queue_enabled?,
16
+ model: :QueueEvent,
17
+ columns: [:recorded_at, :event_type, :job_class, :queue_name, :duration, :id]
18
+ },
19
+ cache: {
20
+ enabled: :solid_cache_enabled?,
21
+ model: :CacheEvent,
22
+ columns: [:recorded_at, :event_type, :hit, :error_class, :duration, :id]
23
+ },
24
+ cable: {
25
+ enabled: :solid_cable_enabled?,
26
+ model: :CableEvent,
27
+ columns: [:recorded_at, :event_type, :channel_class, :broadcasting_digest, :error_class, :duration, :id]
28
+ }
29
+ }.freeze
30
+
31
+ CONNECTION_ERRORS = [
32
+ ActiveRecord::ConnectionNotEstablished,
33
+ ActiveRecord::StatementInvalid,
34
+ *([PG::ConnectionBad] if defined?(PG::ConnectionBad)),
35
+ *([Mysql2::Error::ConnectionError] if defined?(Mysql2::Error::ConnectionError)),
36
+ *([Trilogy::Error] if defined?(Trilogy::Error)),
37
+ *([SQLite3::CantOpenException] if defined?(SQLite3::CantOpenException))
38
+ ].freeze
39
+
40
+ def call(correlation_id:, limit: DEFAULT_LIMIT)
41
+ rows, unavailable = gather_rows(correlation_id)
42
+ effective_limit = limit.to_i
43
+ effective_limit = DEFAULT_LIMIT unless effective_limit.positive?
44
+ Result.new(rows: process_rows(rows, effective_limit), unavailable_components: unavailable)
45
+ end
46
+
47
+ private
48
+
49
+ def gather_rows(correlation_id)
50
+ COMPONENT_CONFIG.each_with_object([[], []]) do |(component, config), (rows, unavailable)|
51
+ next unless SolidObserver.config.public_send(config[:enabled])
52
+
53
+ component_rows = fetch_component_rows(component, config, correlation_id)
54
+ component_rows ? rows.concat(component_rows) : unavailable << component
55
+ end
56
+ end
57
+
58
+ def fetch_component_rows(component, config, correlation_id)
59
+ model = SolidObserver.const_get(config[:model])
60
+ return nil unless data_source_available?(model)
61
+
62
+ pluck_rows(model, component, correlation_id)
63
+ rescue *CONNECTION_ERRORS, TypeError
64
+ nil
65
+ end
66
+
67
+ def data_source_available?(model)
68
+ table_name = model.table_name.to_s
69
+ table_name.present? && model.connection.data_source_exists?(table_name)
70
+ end
71
+
72
+ def pluck_rows(model, component, correlation_id)
73
+ columns = COMPONENT_CONFIG[component][:columns]
74
+ model.where(correlation_id: correlation_id)
75
+ .order(:recorded_at)
76
+ .pluck(*columns)
77
+ .map { |values| build_row(component, columns, values) }
78
+ end
79
+
80
+ def build_row(component, columns, values)
81
+ row = {component: component}
82
+ columns.each_with_index { |column, index| row[column] = values[index] }
83
+ row
84
+ end
85
+
86
+ def process_rows(rows, limit)
87
+ collapsed = collapse_cable_rows(rows)
88
+ capped = apply_cable_cap(collapsed)
89
+ sorted = sort_rows(capped)
90
+ return sorted if capped.size <= limit
91
+
92
+ sort_rows(sorted.last(limit))
93
+ end
94
+
95
+ def collapse_cable_rows(rows)
96
+ cable_rows, other_rows = rows.partition { |row| row[:component] == :cable }
97
+ return rows if cable_rows.empty?
98
+
99
+ other_rows + cable_rows.chunk_while(&method(:collapse_match?)).map(&method(:collapse_group))
100
+ end
101
+
102
+ def collapse_match?(first_row, second_row)
103
+ return false unless first_row[:event_type] == "broadcast" && second_row[:event_type] == "broadcast"
104
+
105
+ digest = first_row[:broadcasting_digest]
106
+ digest.present? && digest == second_row[:broadcasting_digest]
107
+ end
108
+
109
+ def collapse_group(group)
110
+ row = group.first
111
+ size = group.size
112
+ row[:collapsed_count] = size if size > 1
113
+ row.delete(:broadcasting_digest)
114
+ row
115
+ end
116
+
117
+ def apply_cable_cap(rows)
118
+ cable_rows, other_rows = rows.partition { |row| row[:component] == :cable }
119
+ total = cable_rows.size
120
+ return rows if total <= CABLE_CAP
121
+
122
+ other_rows + cap_cable_rows(cable_rows, total)
123
+ end
124
+
125
+ def cap_cable_rows(cable_rows, total)
126
+ keep_count = CABLE_CAP - 1
127
+ [build_summary_row(cable_rows.first(total - keep_count))] + tail_rows(cable_rows, keep_count)
128
+ end
129
+
130
+ def tail_rows(rows, count)
131
+ rows.last(count)
132
+ end
133
+
134
+ def build_summary_row(overflow)
135
+ {
136
+ recorded_at: overflow.last[:recorded_at],
137
+ component: :cable,
138
+ event_type: "cable_summary",
139
+ duration: nil,
140
+ channel_class: nil,
141
+ collapsed_count: overflow.size,
142
+ id: -1
143
+ }
144
+ end
145
+
146
+ def sort_rows(rows)
147
+ rows.sort_by { |row| [row[:recorded_at], COMPONENT_PRIORITY[row[:component]], row[:id] || 0] }
148
+ end
149
+ end
150
+ end
151
+ end
@@ -1,221 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "singleton"
4
- require "concurrent/timer_task"
4
+
5
+ require_relative "event_buffer_core"
5
6
 
6
7
  module SolidObserver
7
8
  # Thread-safe buffer for collecting queue events before batch insertion.
8
- #
9
- # Events are buffered in memory and flushed either when:
10
- # - Buffer size reaches the configured threshold
11
- # - Flush interval timer expires
12
- #
13
- # @example Push an event to the buffer
14
- # QueueEventBuffer.instance.push(event_data)
15
9
  class QueueEventBuffer
16
10
  include Singleton
11
+ include EventBufferCore
17
12
 
18
- INITIAL_METRICS = {
19
- flush_failures_count: 0,
20
- drops_count: 0,
21
- last_flush_at: nil,
22
- last_flush_duration_ms: nil,
23
- last_flush_error: nil
24
- }.freeze
13
+ INITIAL_METRICS = EventBufferCore::INITIAL_METRICS
25
14
 
26
15
  def initialize
27
- @mutex = Mutex.new
28
- @metrics_mutex = Mutex.new
29
- @buffer = []
30
- @metrics = INITIAL_METRICS.dup
31
- @timer_task = nil
32
- end
33
-
34
- # Adds an event to the buffer and triggers flush if threshold reached.
35
- #
36
- # @param event_data [Hash] Event data to buffer
37
- # @return [void]
38
- def push(event_data)
39
- return unless (config = SolidObserver.config).persistence_mode?
40
-
41
- drops_count, should_flush = sync_push_and_check(event_data, config)
42
- record_drop(drops_count) if drops_count.positive?
43
- ensure_timer_running
44
- flush! if should_flush
45
- end
46
-
47
- # Flushes all buffered events to the database.
48
- #
49
- # @return [void]
50
- def flush!
51
- events_to_flush = nil
52
-
53
- @mutex.synchronize do
54
- return if @buffer.empty?
55
- events_to_flush = @buffer.dup
56
- @buffer.clear
57
- end
58
-
59
- started_at_ms = monotonic_ms
60
- begin
61
- Services::FlushEventBuffer.call(events_to_flush)
62
- rescue => e
63
- requeue_failed_events(events_to_flush)
64
- record_flush_failure(e)
65
- Rails.logger&.error "[SolidObserver] Buffer flush failed: #{e.message}" if defined?(Rails)
66
- return
67
- end
68
- record_flush_success(monotonic_ms - started_at_ms)
69
- end
70
-
71
- def size
72
- @mutex.synchronize { @buffer.size }
73
- end
74
-
75
- def clear
76
- @mutex.synchronize { @buffer.clear }
77
- end
78
-
79
- def metrics
80
- current_size = @mutex.synchronize { @buffer.size }
81
- snapshot = @metrics_mutex.synchronize { @metrics.dup }
82
- {
83
- size: current_size,
84
- max_buffer_size: SolidObserver.config.max_buffer_size
85
- }.merge(snapshot)
86
- end
87
-
88
- def shutdown
89
- stop_timer
90
- flush!
16
+ initialize_event_buffer
91
17
  end
92
18
 
93
19
  private
94
20
 
95
- def apply_overflow_policy(event_data, config)
96
- if @buffer.size < config.max_buffer_size
97
- @buffer << event_data
98
- return 0
99
- end
100
-
101
- handle_overflow(event_data, config.buffer_overflow_strategy)
102
- end
103
-
104
- def handle_overflow(event_data, overflow_strategy)
105
- case overflow_strategy
106
- when :drop_old
107
- @buffer.shift
108
- @buffer << event_data
109
- when :drop_new
110
- # No-op: drop incoming event, keep oldest buffered events.
111
- else
112
- raise ArgumentError, "Unsupported buffer_overflow_strategy: #{overflow_strategy.inspect}"
113
- end
114
-
115
- 1
116
- end
117
-
118
- def requeue_failed_events(events_to_flush)
119
- return unless events_to_flush
120
-
121
- dropped_count = sync_requeue_events(events_to_flush, SolidObserver.config.max_buffer_size)
122
- record_drop(dropped_count) if dropped_count.positive?
123
- end
124
-
125
- def trim_events_for_capacity(events, max_buffer_size)
126
- dropped_count = events.size - max_buffer_size
127
- return [events, 0] if dropped_count <= 0
128
-
129
- [events_to_keep(events, max_buffer_size), dropped_count]
130
- end
131
-
132
- def ensure_timer_running
133
- timer_to_start, timer_to_stop = replace_timer_if_stopped
134
- return unless timer_to_start
135
-
136
- timer_to_stop&.shutdown
137
- timer_to_start.execute
138
- end
139
-
140
- def replace_timer_if_stopped
141
- @mutex.synchronize do
142
- current_timer_task = @timer_task
143
- return [nil, nil] if timer_running?(current_timer_task)
144
-
145
- [build_timer_task, current_timer_task]
146
- end
147
- end
148
-
149
- def stop_timer
150
- timer_to_stop = @mutex.synchronize do
151
- current_timer = @timer_task
152
- @timer_task = nil
153
- current_timer
154
- end
155
-
156
- timer_to_stop&.shutdown
157
- end
158
-
159
- def record_flush_success(duration_ms)
160
- @metrics_mutex.synchronize do
161
- @metrics.merge!(
162
- last_flush_at: Time.current,
163
- last_flush_duration_ms: duration_ms,
164
- last_flush_error: nil
165
- )
166
- end
167
- end
168
-
169
- def record_flush_failure(error)
170
- @metrics_mutex.synchronize do
171
- @metrics[:flush_failures_count] += 1
172
- @metrics[:last_flush_error] = error.message
173
- end
174
- end
175
-
176
- def record_drop(count = 1)
177
- @metrics_mutex.synchronize do
178
- @metrics[:drops_count] += count
179
- end
180
- end
181
-
182
- def sync_push_and_check(event_data, config)
183
- @mutex.synchronize do
184
- drops_count = apply_overflow_policy(event_data, config)
185
- [drops_count, @buffer.size >= config.buffer_size]
186
- end
187
- end
188
-
189
- def sync_requeue_events(events_to_flush, max_buffer_size)
190
- @mutex.synchronize do
191
- combined_events = events_to_flush + @buffer
192
- kept_events, dropped_count = trim_events_for_capacity(combined_events, max_buffer_size)
193
- @buffer.replace(kept_events)
194
- dropped_count
195
- end
196
- end
197
-
198
- def events_to_keep(events, max_buffer_size)
199
- if SolidObserver.config.buffer_overflow_strategy == :drop_old
200
- events.last(max_buffer_size)
201
- else
202
- events.first(max_buffer_size)
203
- end
204
- end
205
-
206
- def timer_running?(timer_task)
207
- timer_task && !timer_task.shuttingdown?
208
- end
209
-
210
- def build_timer_task
211
- @timer_task = Concurrent::TimerTask.new(
212
- execution_interval: SolidObserver.config.flush_interval,
213
- run_now: false
214
- ) { flush! }
21
+ def flush_service
22
+ Services::FlushEventBuffer
215
23
  end
216
24
 
217
- def monotonic_ms
218
- Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
25
+ def log_label
26
+ "Buffer"
219
27
  end
220
28
  end
221
29
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidObserver
4
+ module Services
5
+ class CableOperations
6
+ MESSAGES = {
7
+ trim: {
8
+ success: "Expired/trimmable Solid Cable messages trimmed.",
9
+ failure: "Cable trim failed. No raw Cable payloads or adapter details are shown. Use solid_observer:cable:trim if the problem continues."
10
+ }.freeze,
11
+ unavailable: "Cable controls are unavailable because Solid Cable support is disabled or not detected."
12
+ }.freeze
13
+
14
+ class << self
15
+ def available?
16
+ new.available?
17
+ end
18
+
19
+ def trim
20
+ new.trim
21
+ end
22
+
23
+ def message(operation, key = nil)
24
+ return MESSAGES.fetch(:unavailable) if operation == :unavailable
25
+
26
+ MESSAGES.fetch(operation).fetch(key)
27
+ end
28
+
29
+ def unavailable_message
30
+ message(:unavailable)
31
+ end
32
+ end
33
+
34
+ def available?
35
+ SolidObserver.config.solid_cable_enabled? && !!defined?(::SolidCable::Message)
36
+ end
37
+
38
+ def trim
39
+ messages = self.class
40
+ return {ok: false, message: messages.unavailable_message} unless available?
41
+
42
+ perform_operation(
43
+ :trim,
44
+ success_message: messages.message(:trim, :success),
45
+ failure_message: messages.message(:trim, :failure)
46
+ ) do
47
+ trim_cable_messages
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def trim_cable_messages
54
+ if defined?(::SolidCable::TrimJob)
55
+ ::SolidCable::TrimJob.perform_now
56
+ else
57
+ ::SolidCable::Message.trimmable.delete_all
58
+ end
59
+ end
60
+
61
+ def perform_operation(name, success_message:, failure_message:)
62
+ yield
63
+ {ok: true, message: success_message}
64
+ rescue => error
65
+ log_failure(name, error)
66
+ {ok: false, message: failure_message}
67
+ end
68
+
69
+ def log_failure(name, error)
70
+ Rails.logger&.warn("[SolidObserver] Cable #{name} failed: #{error.class}")
71
+ end
72
+ end
73
+ end
74
+ end