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
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "singleton"
|
|
4
|
+
require "concurrent/timer_task"
|
|
5
|
+
|
|
6
|
+
require_relative "services/flush_cable_metrics"
|
|
7
|
+
|
|
8
|
+
module SolidObserver
|
|
9
|
+
class CableMetricBuffer
|
|
10
|
+
include Singleton
|
|
11
|
+
|
|
12
|
+
INITIAL_METRICS = {
|
|
13
|
+
flush_failures_count: 0,
|
|
14
|
+
drops_count: 0,
|
|
15
|
+
last_flush_at: nil,
|
|
16
|
+
last_flush_duration_ms: nil,
|
|
17
|
+
last_flush_error: nil
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@store = MetricStore.new
|
|
22
|
+
@timer_mutex = Mutex.new
|
|
23
|
+
@timer_task = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def increment(metric_data)
|
|
27
|
+
config = SolidObserver.config
|
|
28
|
+
return unless config.persistence_mode?
|
|
29
|
+
|
|
30
|
+
@store.add(metric_data)
|
|
31
|
+
ensure_timer_running
|
|
32
|
+
flush! if size >= config.buffer_size
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def flush!
|
|
36
|
+
metrics_to_flush = @store.drain
|
|
37
|
+
return if metrics_to_flush.empty?
|
|
38
|
+
|
|
39
|
+
flush_metrics(metrics_to_flush, monotonic_ms)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def flush
|
|
43
|
+
flush!
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def size
|
|
47
|
+
@store.size
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def clear
|
|
51
|
+
@store.clear
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def metrics
|
|
55
|
+
@store.metrics
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def shutdown
|
|
59
|
+
stop_timer
|
|
60
|
+
flush!
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def ensure_timer_running
|
|
66
|
+
timer_to_start, timer_to_stop = replace_timer_if_stopped
|
|
67
|
+
return unless timer_to_start
|
|
68
|
+
|
|
69
|
+
timer_to_stop&.shutdown
|
|
70
|
+
timer_to_start.execute
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def replace_timer_if_stopped
|
|
74
|
+
@timer_mutex.synchronize do
|
|
75
|
+
current_timer_task = @timer_task
|
|
76
|
+
return [nil, nil] if current_timer_task && !current_timer_task.shuttingdown?
|
|
77
|
+
|
|
78
|
+
[build_timer_task, current_timer_task]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def stop_timer
|
|
83
|
+
timer_to_stop = @timer_mutex.synchronize do
|
|
84
|
+
current_timer = @timer_task
|
|
85
|
+
@timer_task = nil
|
|
86
|
+
current_timer
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
timer_to_stop&.shutdown
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build_timer_task
|
|
93
|
+
@timer_task = Concurrent::TimerTask.new(
|
|
94
|
+
execution_interval: SolidObserver.config.flush_interval,
|
|
95
|
+
run_now: false
|
|
96
|
+
) { flush! }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def monotonic_ms
|
|
100
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def flush_metrics(metrics_to_flush, started_at_ms)
|
|
104
|
+
Services::FlushCableMetrics.call(metrics_to_flush)
|
|
105
|
+
@store.record_flush_success(monotonic_ms - started_at_ms)
|
|
106
|
+
rescue => error
|
|
107
|
+
handle_flush_error(error, metrics_to_flush)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def handle_flush_error(error, metrics_to_flush)
|
|
111
|
+
@store.requeue(metrics_to_flush)
|
|
112
|
+
@store.record_flush_failure(error)
|
|
113
|
+
Rails.logger&.error("[SolidObserver] Cable metric buffer flush failed: #{error.message}") if defined?(Rails)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class MetricStore
|
|
117
|
+
COUNTERS = %i[
|
|
118
|
+
broadcasts_count
|
|
119
|
+
transmissions_count
|
|
120
|
+
confirmations_count
|
|
121
|
+
rejections_count
|
|
122
|
+
perform_actions_count
|
|
123
|
+
errors_count
|
|
124
|
+
].freeze
|
|
125
|
+
|
|
126
|
+
def initialize
|
|
127
|
+
@mutex = Mutex.new
|
|
128
|
+
@buffer = {}
|
|
129
|
+
@metrics = INITIAL_METRICS.dup
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def add(metric_data)
|
|
133
|
+
@mutex.synchronize { add_metric(metric_data) }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def drain
|
|
137
|
+
@mutex.synchronize do
|
|
138
|
+
drained = @buffer.values.map(&:dup)
|
|
139
|
+
@buffer.clear
|
|
140
|
+
drained
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def requeue(metrics_to_flush)
|
|
145
|
+
@mutex.synchronize { add_metrics_with_capacity(metrics_to_flush + @buffer.values) }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def clear
|
|
149
|
+
@mutex.synchronize { @buffer.clear }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def size
|
|
153
|
+
@mutex.synchronize { @buffer.size }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def metrics
|
|
157
|
+
@mutex.synchronize do
|
|
158
|
+
{
|
|
159
|
+
size: @buffer.size,
|
|
160
|
+
max_buffer_size: SolidObserver.config.max_buffer_size
|
|
161
|
+
}.merge(@metrics.dup)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def record_flush_success(duration_ms)
|
|
166
|
+
@mutex.synchronize do
|
|
167
|
+
@metrics.merge!(
|
|
168
|
+
last_flush_at: Time.current,
|
|
169
|
+
last_flush_duration_ms: duration_ms,
|
|
170
|
+
last_flush_error: nil
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def record_flush_failure(error)
|
|
176
|
+
@mutex.synchronize do
|
|
177
|
+
@metrics[:flush_failures_count] += 1
|
|
178
|
+
@metrics[:last_flush_error] = error.message
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def add_metric(metric_data)
|
|
185
|
+
config = SolidObserver.config
|
|
186
|
+
key = metric_data.fetch(:period_start)
|
|
187
|
+
if @buffer.key?(key)
|
|
188
|
+
merge_metric(@buffer.fetch(key), metric_data)
|
|
189
|
+
elsif @buffer.size < config.max_buffer_size
|
|
190
|
+
@buffer[key] = metric_hash(metric_data)
|
|
191
|
+
else
|
|
192
|
+
handle_overflow(key, metric_data)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def add_metrics_with_capacity(metrics)
|
|
197
|
+
@buffer.clear
|
|
198
|
+
metrics.each { |metric| add_metric(metric) }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def handle_overflow(key, metric_data)
|
|
202
|
+
drop_count = if SolidObserver.config.buffer_overflow_strategy == :drop_old
|
|
203
|
+
replace_old_metric(key, metric_data)
|
|
204
|
+
else
|
|
205
|
+
COUNTERS.sum { |counter| metric_data.fetch(counter, 0).to_i }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
@metrics[:drops_count] += drop_count
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def replace_old_metric(key, metric_data)
|
|
212
|
+
dropped = @buffer.shift&.last
|
|
213
|
+
@buffer[key] = metric_hash(metric_data)
|
|
214
|
+
dropped ? COUNTERS.sum { |counter| dropped.fetch(counter, 0).to_i } : 0
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def merge_metric(target, metric_data)
|
|
218
|
+
COUNTERS.each do |counter|
|
|
219
|
+
target[counter] += metric_data.fetch(counter, 0).to_i
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def metric_hash(metric_data)
|
|
224
|
+
COUNTERS.each_with_object({period_start: metric_data.fetch(:period_start)}) do |counter, hash|
|
|
225
|
+
hash[counter] = metric_data.fetch(counter, 0).to_i
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
class CableSubscriber
|
|
5
|
+
EVENTS = %w[
|
|
6
|
+
broadcast.action_cable
|
|
7
|
+
transmit.action_cable
|
|
8
|
+
transmit_subscription_confirmation.action_cable
|
|
9
|
+
transmit_subscription_rejection.action_cable
|
|
10
|
+
perform_action.action_cable
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
attr_reader :subscriptions
|
|
15
|
+
|
|
16
|
+
def subscribe
|
|
17
|
+
return unless subscription_allowed?
|
|
18
|
+
|
|
19
|
+
self.subscriptions = EVENTS.map { |event_name| subscribe_to(event_name) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def unsubscribe
|
|
23
|
+
return unless subscriptions
|
|
24
|
+
|
|
25
|
+
subscriptions.each { |subscription| ActiveSupport::Notifications.unsubscribe(subscription) }
|
|
26
|
+
self.subscriptions = []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def subscribe!
|
|
30
|
+
subscribe
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def unsubscribe!
|
|
34
|
+
unsubscribe
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def subscribed?
|
|
38
|
+
!!subscriptions&.any?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
attr_writer :subscriptions
|
|
44
|
+
|
|
45
|
+
def subscription_allowed?
|
|
46
|
+
SolidObserver.config.solid_cable_enabled? && !subscribed? && defined?(ActiveSupport::Notifications)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def subscribe_to(event_name)
|
|
50
|
+
ActiveSupport::Notifications.subscribe(event_name) do |*args|
|
|
51
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
52
|
+
Services::RecordCableEvent.call(event: event, buffer: CableEventBuffer.instance)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "singleton"
|
|
4
|
+
|
|
5
|
+
require_relative "event_buffer_core"
|
|
6
|
+
|
|
7
|
+
module SolidObserver
|
|
8
|
+
class CacheEventBuffer
|
|
9
|
+
include Singleton
|
|
10
|
+
include EventBufferCore
|
|
11
|
+
|
|
12
|
+
INITIAL_METRICS = EventBufferCore::INITIAL_METRICS
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
initialize_event_buffer
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def flush_service
|
|
21
|
+
Services::FlushCacheEventBuffer
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def log_label
|
|
25
|
+
"Cache buffer"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "singleton"
|
|
4
|
+
require "concurrent/timer_task"
|
|
5
|
+
|
|
6
|
+
require_relative "services/flush_cache_metrics"
|
|
7
|
+
|
|
8
|
+
module SolidObserver
|
|
9
|
+
class CacheMetricBuffer
|
|
10
|
+
include Singleton
|
|
11
|
+
|
|
12
|
+
INITIAL_METRICS = {
|
|
13
|
+
flush_failures_count: 0,
|
|
14
|
+
drops_count: 0,
|
|
15
|
+
last_flush_at: nil,
|
|
16
|
+
last_flush_duration_ms: nil,
|
|
17
|
+
last_flush_error: nil
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@store = MetricStore.new
|
|
22
|
+
@timer_mutex = Mutex.new
|
|
23
|
+
@timer_task = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def increment(metric_data)
|
|
27
|
+
config = SolidObserver.config
|
|
28
|
+
return unless config.persistence_mode?
|
|
29
|
+
|
|
30
|
+
@store.add(metric_data)
|
|
31
|
+
ensure_timer_running
|
|
32
|
+
flush! if size >= config.buffer_size
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def flush!
|
|
36
|
+
metrics_to_flush = @store.drain
|
|
37
|
+
return if metrics_to_flush.empty?
|
|
38
|
+
|
|
39
|
+
flush_metrics(metrics_to_flush, monotonic_ms)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def flush
|
|
43
|
+
flush!
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def size
|
|
47
|
+
@store.size
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def clear
|
|
51
|
+
@store.clear
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def metrics
|
|
55
|
+
@store.metrics
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def shutdown
|
|
59
|
+
stop_timer
|
|
60
|
+
flush!
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def ensure_timer_running
|
|
66
|
+
timer_to_start, timer_to_stop = replace_timer_if_stopped
|
|
67
|
+
return unless timer_to_start
|
|
68
|
+
|
|
69
|
+
timer_to_stop&.shutdown
|
|
70
|
+
timer_to_start.execute
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def replace_timer_if_stopped
|
|
74
|
+
@timer_mutex.synchronize do
|
|
75
|
+
current_timer_task = @timer_task
|
|
76
|
+
return [nil, nil] if current_timer_task && !current_timer_task.shuttingdown?
|
|
77
|
+
|
|
78
|
+
[build_timer_task, current_timer_task]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def stop_timer
|
|
83
|
+
timer_to_stop = @timer_mutex.synchronize do
|
|
84
|
+
current_timer = @timer_task
|
|
85
|
+
@timer_task = nil
|
|
86
|
+
current_timer
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
timer_to_stop&.shutdown
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build_timer_task
|
|
93
|
+
@timer_task = Concurrent::TimerTask.new(
|
|
94
|
+
execution_interval: SolidObserver.config.flush_interval,
|
|
95
|
+
run_now: false
|
|
96
|
+
) { flush! }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def monotonic_ms
|
|
100
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def flush_metrics(metrics_to_flush, started_at_ms)
|
|
104
|
+
Services::FlushCacheMetrics.call(metrics_to_flush)
|
|
105
|
+
@store.record_flush_success(monotonic_ms - started_at_ms)
|
|
106
|
+
rescue => error
|
|
107
|
+
handle_flush_error(error, metrics_to_flush)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def handle_flush_error(error, metrics_to_flush)
|
|
111
|
+
@store.requeue(metrics_to_flush)
|
|
112
|
+
@store.record_flush_failure(error)
|
|
113
|
+
Rails.logger&.error("[SolidObserver] Cache metric buffer flush failed: #{error.message}") if defined?(Rails)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class MetricStore
|
|
117
|
+
def initialize
|
|
118
|
+
@mutex = Mutex.new
|
|
119
|
+
@buffer = {}
|
|
120
|
+
@metrics = INITIAL_METRICS.dup
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def add(metric_data)
|
|
124
|
+
@mutex.synchronize { add_metric(metric_data) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def drain
|
|
128
|
+
@mutex.synchronize do
|
|
129
|
+
drained = @buffer.values.map(&:dup)
|
|
130
|
+
@buffer.clear
|
|
131
|
+
drained
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def requeue(metrics_to_flush)
|
|
136
|
+
@mutex.synchronize { add_metrics_with_capacity(metrics_to_flush + @buffer.values) }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def clear
|
|
140
|
+
@mutex.synchronize { @buffer.clear }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def size
|
|
144
|
+
@mutex.synchronize { @buffer.size }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def metrics
|
|
148
|
+
@mutex.synchronize do
|
|
149
|
+
{
|
|
150
|
+
size: @buffer.size,
|
|
151
|
+
max_buffer_size: SolidObserver.config.max_buffer_size
|
|
152
|
+
}.merge(@metrics.dup)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def record_flush_success(duration_ms)
|
|
157
|
+
@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
|
+
@mutex.synchronize do
|
|
168
|
+
@metrics[:flush_failures_count] += 1
|
|
169
|
+
@metrics[:last_flush_error] = error.message
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def add_metric(metric_data)
|
|
176
|
+
config = SolidObserver.config
|
|
177
|
+
key = [metric_data.fetch(:event_type), metric_data.fetch(:period_start)]
|
|
178
|
+
if @buffer.key?(key)
|
|
179
|
+
merge_metric(@buffer.fetch(key), metric_data)
|
|
180
|
+
elsif @buffer.size < config.max_buffer_size
|
|
181
|
+
@buffer[key] = metric_hash(metric_data)
|
|
182
|
+
else
|
|
183
|
+
handle_overflow(key, metric_data)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def add_metrics_with_capacity(metrics)
|
|
188
|
+
@buffer.clear
|
|
189
|
+
metrics.each { |metric| add_metric(metric) }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def handle_overflow(key, metric_data)
|
|
193
|
+
drop_count = if SolidObserver.config.buffer_overflow_strategy == :drop_old
|
|
194
|
+
replace_old_metric(key, metric_data)
|
|
195
|
+
else
|
|
196
|
+
metric_data.fetch(:operations_count, 1).to_i
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
@metrics[:drops_count] += drop_count
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def replace_old_metric(key, metric_data)
|
|
203
|
+
dropped = @buffer.shift&.last
|
|
204
|
+
@buffer[key] = metric_hash(metric_data)
|
|
205
|
+
dropped ? dropped.fetch(:operations_count, 1).to_i : 0
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def merge_metric(target, metric_data)
|
|
209
|
+
target[:operations_count] += metric_data.fetch(:operations_count, 1).to_i
|
|
210
|
+
target[:hits_count] += metric_data.fetch(:hits_count, 0).to_i
|
|
211
|
+
target[:misses_count] += metric_data.fetch(:misses_count, 0).to_i
|
|
212
|
+
target[:errors_count] += metric_data.fetch(:errors_count, 0).to_i
|
|
213
|
+
target[:duration_total] += metric_data.fetch(:duration_total, 0.0).to_f
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def metric_hash(metric_data)
|
|
217
|
+
{
|
|
218
|
+
event_type: metric_data.fetch(:event_type),
|
|
219
|
+
period_start: metric_data.fetch(:period_start),
|
|
220
|
+
operations_count: metric_data.fetch(:operations_count, 1).to_i,
|
|
221
|
+
hits_count: metric_data.fetch(:hits_count, 0).to_i,
|
|
222
|
+
misses_count: metric_data.fetch(:misses_count, 0).to_i,
|
|
223
|
+
errors_count: metric_data.fetch(:errors_count, 0).to_i,
|
|
224
|
+
duration_total: metric_data.fetch(:duration_total, 0.0).to_f
|
|
225
|
+
}
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
class CacheSubscriber
|
|
5
|
+
EVENTS = %w[
|
|
6
|
+
cache_read.active_support
|
|
7
|
+
cache_write.active_support
|
|
8
|
+
cache_delete.active_support
|
|
9
|
+
cache_exist?.active_support
|
|
10
|
+
cache_read_multi.active_support
|
|
11
|
+
cache_write_multi.active_support
|
|
12
|
+
cache_delete_multi.active_support
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def subscribe!
|
|
17
|
+
return unless subscription_allowed?
|
|
18
|
+
|
|
19
|
+
@subscriptions = EVENTS.map { |event_name| subscribe_to(event_name) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def unsubscribe!
|
|
23
|
+
return unless @subscriptions
|
|
24
|
+
|
|
25
|
+
@subscriptions.each { |subscription| ActiveSupport::Notifications.unsubscribe(subscription) }
|
|
26
|
+
@subscriptions = []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def subscribed?
|
|
30
|
+
!!@subscriptions&.any?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def subscription_allowed?
|
|
36
|
+
SolidObserver.config.solid_cache_enabled? && !subscribed? && defined?(ActiveSupport::Notifications)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def subscribe_to(event_name)
|
|
40
|
+
ActiveSupport::Notifications.subscribe(event_name) do |*args|
|
|
41
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
42
|
+
Services::RecordCacheEvent.call(event: event, buffer: CacheEventBuffer.instance)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|