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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +80 -20
- data/app/assets/javascripts/solid_observer/live_poll.js +3 -1
- data/app/controllers/solid_observer/application_controller.rb +1 -0
- data/app/controllers/solid_observer/cable_dashboard_controller.rb +52 -0
- data/app/controllers/solid_observer/cable_operations_controller.rb +16 -0
- data/app/controllers/solid_observer/cache_dashboard_controller.rb +33 -40
- data/app/controllers/solid_observer/dashboard_controller.rb +1 -7
- data/app/helpers/solid_observer/application_helper.rb +114 -0
- data/app/models/solid_observer/cable_event.rb +13 -0
- data/app/models/solid_observer/cable_metric.rb +12 -0
- data/app/models/solid_observer/cache_metric.rb +1 -2
- data/app/models/solid_observer/storage_info.rb +1 -1
- data/app/views/layouts/solid_observer/application.html.erb +19 -8
- data/app/views/solid_observer/cable_dashboard/_charts.html.erb +31 -0
- data/app/views/solid_observer/cable_dashboard/_recent_events.html.erb +34 -0
- data/app/views/solid_observer/cable_dashboard/_summary.html.erb +34 -0
- data/app/views/solid_observer/cable_dashboard/index.html.erb +118 -0
- data/app/views/solid_observer/dashboard/_queue_table.html.erb +1 -0
- data/app/views/solid_observer/dashboard/index.html.erb +2 -5
- data/app/views/solid_observer/events/index.html.erb +1 -0
- data/app/views/solid_observer/jobs/index.html.erb +1 -0
- data/app/views/solid_observer/storages/show.html.erb +29 -3
- data/config/routes.rb +2 -0
- data/db/migrate/20260612000001_add_event_type_recorded_at_index_to_cache_events.rb +21 -0
- data/db/migrate/20260619000001_create_solid_observer_cable_events.rb +22 -0
- data/db/migrate/20260619000002_create_solid_observer_cable_metrics.rb +17 -0
- data/lib/generators/solid_observer/install_generator.rb +8 -1
- data/lib/generators/solid_observer/templates/initializer.rb.tt +18 -3
- data/lib/solid_observer/base_event.rb +1 -1
- data/lib/solid_observer/base_metric.rb +1 -1
- data/lib/solid_observer/base_record.rb +8 -0
- data/lib/solid_observer/cable_event_buffer.rb +28 -0
- data/lib/solid_observer/cable_metric_buffer.rb +230 -0
- data/lib/solid_observer/cable_subscriber.rb +57 -0
- data/lib/solid_observer/cache_event_buffer.rb +11 -36
- data/lib/solid_observer/cache_metric_buffer.rb +229 -0
- data/lib/solid_observer/chart_buffer.rb +84 -27
- data/lib/solid_observer/configuration.rb +47 -4
- data/lib/solid_observer/engine.rb +46 -28
- data/lib/solid_observer/event_buffer_core.rb +218 -0
- data/lib/solid_observer/queue_event_buffer.rb +9 -201
- data/lib/solid_observer/services/cable_operations.rb +74 -0
- data/lib/solid_observer/services/cable_stats.rb +385 -0
- data/lib/solid_observer/services/cache_stats.rb +35 -18
- data/lib/solid_observer/services/cleanup_storage.rb +82 -47
- data/lib/solid_observer/services/flush_cable_event_buffer.rb +54 -0
- data/lib/solid_observer/services/flush_cable_metrics.rb +54 -0
- data/lib/solid_observer/services/flush_cache_metrics.rb +56 -0
- data/lib/solid_observer/services/record_cable_event.rb +114 -0
- data/lib/solid_observer/services/record_cable_metric.rb +73 -0
- data/lib/solid_observer/services/record_cache_event.rb +23 -0
- data/lib/solid_observer/services/record_cache_metric.rb +13 -21
- data/lib/solid_observer/services/storage_info_snapshot.rb +103 -15
- data/lib/solid_observer/version.rb +1 -1
- data/lib/solid_observer.rb +36 -11
- data/lib/tasks/solid_observer.rake +84 -23
- metadata +26 -6
- data/app/assets/stylesheets/solid_observer/application.css +0 -18
- data/bin/console +0 -11
- data/bin/quality_gate +0 -95
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
110
|
+
def cache_store
|
|
111
|
+
return unless defined?(Rails)
|
|
112
|
+
|
|
113
|
+
Rails.cache
|
|
77
114
|
end
|
|
78
115
|
|
|
79
|
-
def
|
|
80
|
-
(
|
|
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
|
|
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,
|
|
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
|
-
|
|
53
|
-
|
|
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::
|
|
71
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|