solid_observer 0.4.0 → 0.5.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +80 -20
  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 +1 -7
  10. data/app/helpers/solid_observer/application_helper.rb +114 -0
  11. data/app/models/solid_observer/cable_event.rb +13 -0
  12. data/app/models/solid_observer/cable_metric.rb +12 -0
  13. data/app/models/solid_observer/cache_metric.rb +1 -2
  14. data/app/models/solid_observer/storage_info.rb +1 -1
  15. data/app/views/layouts/solid_observer/application.html.erb +19 -8
  16. data/app/views/solid_observer/cable_dashboard/_charts.html.erb +31 -0
  17. data/app/views/solid_observer/cable_dashboard/_recent_events.html.erb +34 -0
  18. data/app/views/solid_observer/cable_dashboard/_summary.html.erb +34 -0
  19. data/app/views/solid_observer/cable_dashboard/index.html.erb +118 -0
  20. data/app/views/solid_observer/dashboard/_queue_table.html.erb +1 -0
  21. data/app/views/solid_observer/dashboard/index.html.erb +2 -5
  22. data/app/views/solid_observer/events/index.html.erb +1 -0
  23. data/app/views/solid_observer/jobs/index.html.erb +1 -0
  24. data/app/views/solid_observer/storages/show.html.erb +29 -3
  25. data/config/routes.rb +2 -0
  26. data/db/migrate/20260612000001_add_event_type_recorded_at_index_to_cache_events.rb +21 -0
  27. data/db/migrate/20260619000001_create_solid_observer_cable_events.rb +22 -0
  28. data/db/migrate/20260619000002_create_solid_observer_cable_metrics.rb +17 -0
  29. data/lib/generators/solid_observer/install_generator.rb +8 -1
  30. data/lib/generators/solid_observer/templates/initializer.rb.tt +18 -3
  31. data/lib/solid_observer/base_event.rb +1 -1
  32. data/lib/solid_observer/base_metric.rb +1 -1
  33. data/lib/solid_observer/base_record.rb +8 -0
  34. data/lib/solid_observer/cable_event_buffer.rb +28 -0
  35. data/lib/solid_observer/cable_metric_buffer.rb +230 -0
  36. data/lib/solid_observer/cable_subscriber.rb +57 -0
  37. data/lib/solid_observer/cache_event_buffer.rb +11 -36
  38. data/lib/solid_observer/cache_metric_buffer.rb +229 -0
  39. data/lib/solid_observer/chart_buffer.rb +84 -27
  40. data/lib/solid_observer/configuration.rb +47 -4
  41. data/lib/solid_observer/engine.rb +46 -28
  42. data/lib/solid_observer/event_buffer_core.rb +218 -0
  43. data/lib/solid_observer/queue_event_buffer.rb +9 -201
  44. data/lib/solid_observer/services/cable_operations.rb +74 -0
  45. data/lib/solid_observer/services/cable_stats.rb +385 -0
  46. data/lib/solid_observer/services/cache_stats.rb +35 -18
  47. data/lib/solid_observer/services/cleanup_storage.rb +82 -47
  48. data/lib/solid_observer/services/flush_cable_event_buffer.rb +54 -0
  49. data/lib/solid_observer/services/flush_cable_metrics.rb +54 -0
  50. data/lib/solid_observer/services/flush_cache_metrics.rb +56 -0
  51. data/lib/solid_observer/services/record_cable_event.rb +114 -0
  52. data/lib/solid_observer/services/record_cable_metric.rb +73 -0
  53. data/lib/solid_observer/services/record_cache_event.rb +23 -0
  54. data/lib/solid_observer/services/record_cache_metric.rb +13 -21
  55. data/lib/solid_observer/services/storage_info_snapshot.rb +103 -15
  56. data/lib/solid_observer/version.rb +1 -1
  57. data/lib/solid_observer.rb +36 -11
  58. data/lib/tasks/solid_observer.rake +84 -23
  59. metadata +26 -6
  60. data/app/assets/stylesheets/solid_observer/application.css +0 -18
  61. data/bin/console +0 -11
  62. data/bin/quality_gate +0 -95
  63. 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
@@ -22,13 +22,17 @@ module SolidObserver
22
22
  attr_accessor :observe_queue
23
23
 
24
24
  # Observer Settings (planned for a future release)
25
- # @note Cache and Cable observers are not yet implemented
25
+ # @note Cable observer is not yet fully implemented
26
26
  attr_accessor :observe_cache,
27
27
  :observe_cable,
28
28
  :cache_sampling_rate,
29
29
  :cache_slow_threshold,
30
30
  :cache_store_errors
31
31
 
32
+ attr_reader :cable_rejection_threshold,
33
+ :cable_backlog_threshold,
34
+ :cable_error_threshold
35
+
32
36
  # Retention Settings
33
37
  attr_accessor :event_retention
34
38
 
@@ -49,7 +53,8 @@ module SolidObserver
49
53
  :flush_interval,
50
54
  :max_buffer_size,
51
55
  :buffer_overflow_strategy,
52
- :filter_cache_ttl
56
+ :filter_cache_ttl,
57
+ :cable_sampling_rate
53
58
 
54
59
  # Correlation Settings
55
60
  attr_accessor :correlation_id_generator
@@ -58,13 +63,16 @@ module SolidObserver
58
63
  @ui_enabled, @ui_base_controller, @ui_username, @ui_password,
59
64
  @storage_mode, @observe_queue, @observe_cache, @observe_cable,
60
65
  @event_retention, @metrics_retention, @max_db_size, @warning_threshold,
61
- @sampling_rate, @cache_sampling_rate, @cache_slow_threshold, @cache_store_errors,
66
+ @sampling_rate, @cache_sampling_rate, @cable_sampling_rate, @cache_slow_threshold, @cache_store_errors,
67
+ @cable_rejection_threshold, @cable_backlog_threshold, @cable_error_threshold,
62
68
  @buffer_size, @flush_interval,
63
69
  @max_buffer_size, @buffer_overflow_strategy, @filter_cache_ttl,
64
70
  @correlation_id_generator = !production?, "::ApplicationController", nil, nil,
65
71
  :persistence, true, false, false,
66
72
  30.days, 90.days, 1.gigabyte, 0.8,
67
- 1.0, 0.1, 0.1, true, 1000, 10.seconds,
73
+ 1.0, 0.1, 0.1, 0.1, true,
74
+ 0.05, 0.10, 0.0,
75
+ 1000, 10.seconds,
68
76
  10_000, :drop_old, 1.minute,
69
77
  nil
70
78
  end
@@ -95,6 +103,10 @@ module SolidObserver
95
103
  !!defined?(::SolidCache)
96
104
  end
97
105
 
106
+ def solid_cable_available?
107
+ !!defined?(::SolidCable)
108
+ end
109
+
98
110
  def solid_queue_enabled?
99
111
  observe_queue
100
112
  end
@@ -103,11 +115,35 @@ module SolidObserver
103
115
  observe_cache && solid_cache_available?
104
116
  end
105
117
 
118
+ def solid_cable_enabled?
119
+ observe_cable && solid_cable_available?
120
+ end
121
+
106
122
  def sampling_rate=(value)
107
123
  validate_rate!(:sampling_rate, value)
108
124
  @sampling_rate = value
109
125
  end
110
126
 
127
+ def cable_sampling_rate=(value)
128
+ validate_rate!(:cable_sampling_rate, value)
129
+ @cable_sampling_rate = value
130
+ end
131
+
132
+ def cable_rejection_threshold=(value)
133
+ validate_rate!(:cable_rejection_threshold, value)
134
+ @cable_rejection_threshold = value
135
+ end
136
+
137
+ def cable_backlog_threshold=(value)
138
+ validate_rate!(:cable_backlog_threshold, value)
139
+ @cable_backlog_threshold = value
140
+ end
141
+
142
+ def cable_error_threshold=(value)
143
+ validate_non_negative_numeric!(:cable_error_threshold, value)
144
+ @cable_error_threshold = value
145
+ end
146
+
111
147
  def warning_threshold=(value)
112
148
  validate_rate!(:warning_threshold, value)
113
149
  @warning_threshold = value
@@ -170,6 +206,13 @@ module SolidObserver
170
206
  end
171
207
  end
172
208
 
209
+ # :reek:FeatureEnvy
210
+ def validate_non_negative_numeric!(name, value)
211
+ unless value.is_a?(Numeric) && value >= 0
212
+ raise ArgumentError, "#{name} must be a non-negative number"
213
+ end
214
+ end
215
+
173
216
  def production?
174
217
  defined?(Rails) && Rails.env.production?
175
218
  end
@@ -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
@@ -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