solid_observer 0.3.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 +34 -0
- data/README.md +195 -82
- 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 +52 -0
- data/app/controllers/solid_observer/cache_operations_controller.rb +24 -0
- data/app/controllers/solid_observer/dashboard_controller.rb +38 -1
- data/app/controllers/solid_observer/storages_controller.rb +1 -1
- data/app/helpers/solid_observer/application_helper.rb +268 -5
- data/app/helpers/solid_observer/dashboard_helper.rb +30 -11
- 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_event.rb +15 -0
- data/app/models/solid_observer/cache_metric.rb +13 -0
- data/app/models/solid_observer/storage_info.rb +4 -1
- data/app/views/layouts/solid_observer/application.html.erb +157 -19
- 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/cache_dashboard/_charts.html.erb +40 -0
- data/app/views/solid_observer/cache_dashboard/_recent_events.html.erb +34 -0
- data/app/views/solid_observer/cache_dashboard/_summary.html.erb +39 -0
- data/app/views/solid_observer/cache_dashboard/index.html.erb +62 -0
- data/app/views/solid_observer/cache_operations/_confirm_clear.html.erb +6 -0
- data/app/views/solid_observer/cache_operations/index.html.erb +60 -0
- data/app/views/solid_observer/dashboard/_queue_table.html.erb +1 -0
- data/app/views/solid_observer/dashboard/index.html.erb +32 -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/jobs/show.html.erb +3 -3
- data/app/views/solid_observer/storages/show.html.erb +90 -32
- data/config/routes.rb +7 -0
- data/db/migrate/20260601000001_create_solid_observer_cache_events.rb +22 -0
- data/db/migrate/20260601000002_create_solid_observer_cache_metrics.rb +18 -0
- data/db/migrate/20260602000001_add_component_to_solid_observer_storage_infos.rb +8 -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 +20 -4
- 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 +28 -0
- data/lib/solid_observer/cache_metric_buffer.rb +229 -0
- data/lib/solid_observer/cache_subscriber.rb +47 -0
- data/lib/solid_observer/chart_buffer.rb +84 -27
- data/lib/solid_observer/cli/storage.rb +16 -13
- data/lib/solid_observer/configuration.rb +67 -5
- data/lib/solid_observer/engine.rb +70 -15
- 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_operations.rb +115 -0
- data/lib/solid_observer/services/cache_stats.rb +346 -0
- data/lib/solid_observer/services/cleanup_storage.rb +98 -47
- data/lib/solid_observer/services/database_size.rb +13 -8
- 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_event_buffer.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 +165 -0
- data/lib/solid_observer/services/record_cache_metric.rb +66 -0
- data/lib/solid_observer/services/storage_info_snapshot.rb +216 -0
- data/lib/solid_observer/version.rb +1 -1
- data/lib/solid_observer.rb +36 -11
- data/lib/tasks/solid_observer.rake +111 -21
- metadata +47 -5
- 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
|
|
@@ -28,47 +28,50 @@ module SolidObserver
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def gather_storage_stats
|
|
31
|
-
{
|
|
32
|
-
db_size_bytes: SolidObserver::Services::DatabaseSize.call(connection: QueueEvent.connection),
|
|
33
|
-
event_count: QueueEvent.count,
|
|
34
|
-
max_size_bytes: SolidObserver.config.max_db_size
|
|
35
|
-
}
|
|
31
|
+
{components: SolidObserver::Services::StorageInfoSnapshot.call, max_size_bytes: SolidObserver.config.max_db_size}
|
|
36
32
|
rescue => e
|
|
37
33
|
{error: "Failed to gather storage stats: #{e.message}"}
|
|
38
34
|
end
|
|
39
35
|
|
|
40
36
|
def print_storage_table(stats)
|
|
41
|
-
event_count = stats[:event_count]
|
|
42
|
-
db_size_bytes = stats[:db_size_bytes]
|
|
43
37
|
max_size_bytes = stats[:max_size_bytes]
|
|
38
|
+
components = stats[:components]
|
|
44
39
|
|
|
45
40
|
table(
|
|
46
41
|
headers: ["Component", "Size", "Events", "Usage", "Status"],
|
|
47
|
-
rows:
|
|
42
|
+
rows: components.map { |component| storage_row(component: component, max_size_bytes: max_size_bytes) }
|
|
48
43
|
)
|
|
49
44
|
|
|
50
45
|
output("")
|
|
51
46
|
end
|
|
52
47
|
|
|
53
|
-
def storage_row(
|
|
54
|
-
|
|
48
|
+
def storage_row(component:, max_size_bytes:)
|
|
49
|
+
event_count = component[:event_count]
|
|
50
|
+
size, usage, status = storage_displays(component: component, max_size_bytes: max_size_bytes)
|
|
55
51
|
|
|
56
52
|
[
|
|
57
|
-
|
|
53
|
+
component[:label],
|
|
58
54
|
size,
|
|
59
|
-
format_number(event_count),
|
|
55
|
+
event_count ? format_number(event_count) : "—",
|
|
60
56
|
usage,
|
|
61
57
|
status
|
|
62
58
|
]
|
|
63
59
|
end
|
|
64
60
|
|
|
65
|
-
def storage_displays(
|
|
61
|
+
def storage_displays(component:, max_size_bytes:)
|
|
62
|
+
return unavailable_displays unless component[:available]
|
|
63
|
+
|
|
64
|
+
db_size_bytes = component[:db_size_bytes]
|
|
66
65
|
return ["N/A", "N/A", "— Unknown"] unless db_size_bytes
|
|
67
66
|
|
|
68
67
|
percentage = calculate_percentage(db_size_bytes, max_size_bytes)
|
|
69
68
|
[format_size(bytes_to_mb(db_size_bytes)), "#{percentage}%", status_indicator(percentage)]
|
|
70
69
|
end
|
|
71
70
|
|
|
71
|
+
def unavailable_displays
|
|
72
|
+
["—", "—", "— Unavailable"]
|
|
73
|
+
end
|
|
74
|
+
|
|
72
75
|
def print_configuration
|
|
73
76
|
retention_days = (SolidObserver.config.event_retention / 1.day).to_i
|
|
74
77
|
max_size_mb = bytes_to_mb(SolidObserver.config.max_db_size)
|
|
@@ -22,10 +22,16 @@ 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
|
-
:cache_sampling_rate
|
|
28
|
+
:cache_sampling_rate,
|
|
29
|
+
:cache_slow_threshold,
|
|
30
|
+
:cache_store_errors
|
|
31
|
+
|
|
32
|
+
attr_reader :cable_rejection_threshold,
|
|
33
|
+
:cable_backlog_threshold,
|
|
34
|
+
:cable_error_threshold
|
|
29
35
|
|
|
30
36
|
# Retention Settings
|
|
31
37
|
attr_accessor :event_retention
|
|
@@ -47,7 +53,8 @@ module SolidObserver
|
|
|
47
53
|
:flush_interval,
|
|
48
54
|
:max_buffer_size,
|
|
49
55
|
:buffer_overflow_strategy,
|
|
50
|
-
:filter_cache_ttl
|
|
56
|
+
:filter_cache_ttl,
|
|
57
|
+
:cable_sampling_rate
|
|
51
58
|
|
|
52
59
|
# Correlation Settings
|
|
53
60
|
attr_accessor :correlation_id_generator
|
|
@@ -56,12 +63,16 @@ module SolidObserver
|
|
|
56
63
|
@ui_enabled, @ui_base_controller, @ui_username, @ui_password,
|
|
57
64
|
@storage_mode, @observe_queue, @observe_cache, @observe_cable,
|
|
58
65
|
@event_retention, @metrics_retention, @max_db_size, @warning_threshold,
|
|
59
|
-
@sampling_rate, @cache_sampling_rate, @
|
|
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,
|
|
68
|
+
@buffer_size, @flush_interval,
|
|
60
69
|
@max_buffer_size, @buffer_overflow_strategy, @filter_cache_ttl,
|
|
61
70
|
@correlation_id_generator = !production?, "::ApplicationController", nil, nil,
|
|
62
71
|
:persistence, true, false, false,
|
|
63
72
|
30.days, 90.days, 1.gigabyte, 0.8,
|
|
64
|
-
1.0, 0.1,
|
|
73
|
+
1.0, 0.1, 0.1, 0.1, true,
|
|
74
|
+
0.05, 0.10, 0.0,
|
|
75
|
+
1000, 10.seconds,
|
|
65
76
|
10_000, :drop_old, 1.minute,
|
|
66
77
|
nil
|
|
67
78
|
end
|
|
@@ -84,11 +95,55 @@ module SolidObserver
|
|
|
84
95
|
@storage_mode == :realtime
|
|
85
96
|
end
|
|
86
97
|
|
|
98
|
+
def solid_queue_available?
|
|
99
|
+
!!defined?(::SolidQueue)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def solid_cache_available?
|
|
103
|
+
!!defined?(::SolidCache)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def solid_cable_available?
|
|
107
|
+
!!defined?(::SolidCable)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def solid_queue_enabled?
|
|
111
|
+
observe_queue
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def solid_cache_enabled?
|
|
115
|
+
observe_cache && solid_cache_available?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def solid_cable_enabled?
|
|
119
|
+
observe_cable && solid_cable_available?
|
|
120
|
+
end
|
|
121
|
+
|
|
87
122
|
def sampling_rate=(value)
|
|
88
123
|
validate_rate!(:sampling_rate, value)
|
|
89
124
|
@sampling_rate = value
|
|
90
125
|
end
|
|
91
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
|
+
|
|
92
147
|
def warning_threshold=(value)
|
|
93
148
|
validate_rate!(:warning_threshold, value)
|
|
94
149
|
@warning_threshold = value
|
|
@@ -151,6 +206,13 @@ module SolidObserver
|
|
|
151
206
|
end
|
|
152
207
|
end
|
|
153
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
|
+
|
|
154
216
|
def production?
|
|
155
217
|
defined?(Rails) && Rails.env.production?
|
|
156
218
|
end
|
|
@@ -10,11 +10,25 @@ module SolidObserver
|
|
|
10
10
|
|
|
11
11
|
class << self
|
|
12
12
|
def check_solid_queue_availability
|
|
13
|
-
return if defined?(SolidQueue)
|
|
13
|
+
return if defined?(::SolidQueue)
|
|
14
14
|
|
|
15
15
|
Rails.logger.warn "[SolidObserver] SolidQueue not detected. Queue observability features will be limited."
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
def check_solid_cache_availability
|
|
19
|
+
return if defined?(::SolidCache)
|
|
20
|
+
return unless SolidObserver.config.observe_cache
|
|
21
|
+
|
|
22
|
+
Rails.logger.warn "[SolidObserver] SolidCache not detected. Cache observability features will be disabled."
|
|
23
|
+
end
|
|
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
|
+
|
|
18
32
|
def check_ui_authentication
|
|
19
33
|
Services::UiAuthCheck.call(config: SolidObserver.config)
|
|
20
34
|
end
|
|
@@ -28,15 +42,18 @@ module SolidObserver
|
|
|
28
42
|
|
|
29
43
|
def activate_subscribers
|
|
30
44
|
return activate_subscribers_in_realtime if SolidObserver.config.realtime_mode?
|
|
31
|
-
return if activation_skipped_for_table_status?
|
|
32
45
|
|
|
33
46
|
Rails.logger.info "[SolidObserver] Activating event subscribers"
|
|
34
|
-
|
|
47
|
+
activate_queue_subscriber
|
|
48
|
+
activate_cache_subscriber
|
|
49
|
+
activate_cable_subscriber
|
|
35
50
|
end
|
|
36
51
|
|
|
37
52
|
private
|
|
38
53
|
|
|
39
54
|
def queue_db_config
|
|
55
|
+
return unless active_record_available?
|
|
56
|
+
|
|
40
57
|
ActiveRecord::Base.configurations.configs_for(
|
|
41
58
|
env_name: Rails.env,
|
|
42
59
|
name: "solid_observer_queue"
|
|
@@ -48,40 +65,73 @@ module SolidObserver
|
|
|
48
65
|
database: {writing: :solid_observer_queue, reading: :solid_observer_queue}
|
|
49
66
|
}
|
|
50
67
|
|
|
51
|
-
SolidObserver::
|
|
52
|
-
|
|
68
|
+
SolidObserver::BaseRecord.connects_to(**connection_config)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def active_record_available?
|
|
72
|
+
defined?(::ActiveRecord::Base)
|
|
53
73
|
end
|
|
54
74
|
|
|
55
75
|
def activate_subscribers_in_realtime
|
|
56
76
|
Rails.logger.info "[SolidObserver] Starting in real-time mode (no persistence)"
|
|
57
|
-
Subscriber.subscribe!
|
|
77
|
+
Subscriber.subscribe! if should_activate_queue_subscriber?
|
|
78
|
+
CacheSubscriber.subscribe! if should_activate_cache_subscriber?
|
|
79
|
+
CableSubscriber.subscribe! if should_activate_cable_subscriber?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def activate_queue_subscriber
|
|
83
|
+
activate_subscriber_for_table("solid_observer_queue_events", Subscriber) if should_activate_queue_subscriber?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def activate_cache_subscriber
|
|
87
|
+
activate_subscriber_for_table("solid_observer_cache_events", CacheSubscriber) if should_activate_cache_subscriber?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def activate_cable_subscriber
|
|
91
|
+
activate_subscriber_for_table("solid_observer_cable_events", CableSubscriber) if should_activate_cable_subscriber?
|
|
58
92
|
end
|
|
59
93
|
|
|
60
|
-
def
|
|
61
|
-
case table_status(
|
|
94
|
+
def activate_subscriber_for_table(table_name, subscriber)
|
|
95
|
+
case table_status(table_name)
|
|
62
96
|
when :absent
|
|
63
|
-
log_activation_skip("Tables not found. Run: rails solid_observer:install:migrations && rails db:migrate")
|
|
64
|
-
true
|
|
97
|
+
log_activation_skip("Tables not found (missing: #{table_name}). Run: rails solid_observer:install:migrations && rails db:migrate")
|
|
65
98
|
when :unknown
|
|
66
99
|
log_activation_skip("Database not reachable at boot. Skipping subscriber activation.")
|
|
67
|
-
true
|
|
68
100
|
else
|
|
69
|
-
|
|
101
|
+
subscriber.subscribe!
|
|
70
102
|
end
|
|
71
103
|
end
|
|
72
104
|
|
|
105
|
+
def should_activate_queue_subscriber?
|
|
106
|
+
SolidObserver.config.solid_queue_enabled?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def should_activate_cache_subscriber?
|
|
110
|
+
SolidObserver.config.solid_cache_enabled?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def should_activate_cable_subscriber?
|
|
114
|
+
SolidObserver.config.solid_cable_enabled?
|
|
115
|
+
end
|
|
116
|
+
|
|
73
117
|
def log_activation_skip(message)
|
|
74
118
|
Rails.logger.info("[SolidObserver] #{message}")
|
|
75
119
|
end
|
|
76
120
|
|
|
77
121
|
def table_status(table_name)
|
|
78
|
-
|
|
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
|
|
79
131
|
|
|
80
132
|
return :present if cached_data_source_exists?(pool, table_name)
|
|
81
133
|
|
|
82
134
|
data_source_exists_in_db?(pool, table_name) ? :present : :absent
|
|
83
|
-
rescue *boot_connection_errors
|
|
84
|
-
:unknown
|
|
85
135
|
end
|
|
86
136
|
|
|
87
137
|
def cached_data_source_exists?(pool, table_name)
|
|
@@ -96,12 +146,15 @@ module SolidObserver
|
|
|
96
146
|
end
|
|
97
147
|
|
|
98
148
|
def boot_connection_errors
|
|
149
|
+
return [] unless active_record_available?
|
|
150
|
+
|
|
99
151
|
[
|
|
100
152
|
ActiveRecord::NoDatabaseError,
|
|
101
153
|
ActiveRecord::ConnectionNotEstablished,
|
|
102
154
|
ActiveRecord::StatementInvalid,
|
|
103
155
|
*([PG::ConnectionBad] if defined?(PG::ConnectionBad)),
|
|
104
156
|
*([Mysql2::Error::ConnectionError] if defined?(Mysql2::Error::ConnectionError)),
|
|
157
|
+
*([Trilogy::Error] if defined?(Trilogy::Error)),
|
|
105
158
|
*([SQLite3::CantOpenException] if defined?(SQLite3::CantOpenException))
|
|
106
159
|
]
|
|
107
160
|
end
|
|
@@ -109,6 +162,8 @@ module SolidObserver
|
|
|
109
162
|
|
|
110
163
|
config.before_initialize do
|
|
111
164
|
Engine.check_solid_queue_availability
|
|
165
|
+
Engine.check_solid_cache_availability
|
|
166
|
+
Engine.check_solid_cable_availability
|
|
112
167
|
end
|
|
113
168
|
|
|
114
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
|