solid_observer 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/README.md +100 -25
- 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 +21 -13
- data/app/controllers/solid_observer/traces_controller.rb +17 -0
- data/app/helpers/solid_observer/application_helper.rb +145 -3
- 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 +35 -9
- data/app/views/solid_observer/cable_dashboard/_charts.html.erb +31 -0
- data/app/views/solid_observer/cable_dashboard/_recent_events.html.erb +39 -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/_recent_events.html.erb +6 -1
- data/app/views/solid_observer/dashboard/_component_cards.html.erb +18 -0
- data/app/views/solid_observer/dashboard/_health.html.erb +6 -0
- data/app/views/solid_observer/dashboard/_home.html.erb +10 -0
- data/app/views/solid_observer/dashboard/_queue.html.erb +138 -0
- data/app/views/solid_observer/dashboard/_queue_table.html.erb +1 -0
- data/app/views/solid_observer/dashboard/_unified_feed.html.erb +38 -0
- data/app/views/solid_observer/dashboard/index.html.erb +3 -139
- data/app/views/solid_observer/events/index.html.erb +7 -1
- data/app/views/solid_observer/jobs/index.html.erb +1 -0
- data/app/views/solid_observer/storages/show.html.erb +29 -3
- data/app/views/solid_observer/traces/show.html.erb +80 -0
- data/config/routes.rb +3 -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/db/migrate/20260630000001_add_correlation_id_to_cache_events.rb +8 -0
- data/db/migrate/20260630000002_add_correlation_id_to_cable_events.rb +8 -0
- data/lib/generators/solid_observer/install_generator.rb +8 -1
- data/lib/generators/solid_observer/templates/initializer.rb.tt +24 -5
- 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/cli/health.rb +49 -0
- data/lib/solid_observer/cli/trace.rb +106 -0
- data/lib/solid_observer/configuration.rb +47 -7
- data/lib/solid_observer/correlated.rb +23 -0
- data/lib/solid_observer/engine.rb +46 -28
- data/lib/solid_observer/event_buffer_core.rb +218 -0
- data/lib/solid_observer/queries/trace_query.rb +151 -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/health_score.rb +85 -0
- data/lib/solid_observer/services/record_cable_event.rb +115 -0
- data/lib/solid_observer/services/record_cable_metric.rb +73 -0
- data/lib/solid_observer/services/record_cache_event.rb +24 -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/services/unified_feed.rb +101 -0
- data/lib/solid_observer/version.rb +1 -1
- data/lib/solid_observer.rb +42 -11
- data/lib/tasks/solid_observer.rake +97 -21
- metadata +41 -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
|
@@ -9,11 +9,20 @@ SolidObserver.configure do |config|
|
|
|
9
9
|
# Recommended: false in production, true in development/staging
|
|
10
10
|
config.ui_enabled = !Rails.env.production?
|
|
11
11
|
|
|
12
|
-
#
|
|
12
|
+
# Production dashboard exposure is the host app's responsibility. If you mount
|
|
13
|
+
# the dashboard in production, wrap the mount in your existing admin auth
|
|
14
|
+
# constraint, for example in config/routes.rb:
|
|
15
|
+
#
|
|
16
|
+
# authenticate :user, ->(user) { user.admin? } do
|
|
17
|
+
# mount SolidObserver::Engine, at: "/solid_observer"
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# Built-in HTTP Basic Auth is enabled only when BOTH credentials are present.
|
|
21
|
+
# Setting only one credential disables Basic Auth and logs a boot warning.
|
|
13
22
|
# config.ui_username = "admin"
|
|
14
23
|
# config.ui_password = "secret"
|
|
15
24
|
|
|
16
|
-
# Base controller
|
|
25
|
+
# Base controller used only to detect API-only rendering compatibility
|
|
17
26
|
# config.ui_base_controller = "ApplicationController"
|
|
18
27
|
|
|
19
28
|
# === Queue Observability (v0.1.0) ===
|
|
@@ -24,8 +33,17 @@ SolidObserver.configure do |config|
|
|
|
24
33
|
# config.observe_cache = true
|
|
25
34
|
# config.cache_sampling_rate = 0.1 # Sample 10% of cache operations
|
|
26
35
|
|
|
27
|
-
# === Cable Observability (
|
|
36
|
+
# === Cable Observability (v0.5.0) ===
|
|
37
|
+
# Enable SolidCable event capture and trim controls.
|
|
38
|
+
# Requires SolidCable in the host app; SolidObserver does not add it.
|
|
28
39
|
# config.observe_cable = true
|
|
40
|
+
# config.cable_sampling_rate = 0.1 # Sample 10% of broadcast events
|
|
41
|
+
# config.cable_rejection_threshold = 0.05 # Rejection rate → Degraded stability
|
|
42
|
+
# config.cable_backlog_threshold = 0.10 # Backlog ratio threshold
|
|
43
|
+
# config.cable_error_threshold = 0.0 # Error rate threshold
|
|
44
|
+
# === Trace & Health Observability (v0.6.0) ===
|
|
45
|
+
# Health scoring (Services::HealthScore) is auto-computed and config-free.
|
|
46
|
+
# Cross-component trace lookup uses correlation_id across Queue, Cache, and Cable.
|
|
29
47
|
|
|
30
48
|
# Data Retention
|
|
31
49
|
config.event_retention = 30.days # How long to keep event records
|
|
@@ -42,9 +60,10 @@ SolidObserver.configure do |config|
|
|
|
42
60
|
# Sampling (reduce overhead in high-traffic apps)
|
|
43
61
|
config.sampling_rate = 1.0 # 1.0 = 100% (capture all events)
|
|
44
62
|
|
|
45
|
-
# Correlation ID (for
|
|
63
|
+
# Correlation ID (for cross-component tracing & APM integration)
|
|
64
|
+
# Stamped correlation_id powers `rails solid_observer:trace[CORRELATION_ID]` CLI
|
|
65
|
+
# and the Web UI at /solid_observer/traces/:correlation_id.
|
|
46
66
|
# Integrate with your APM tool (Datadog, Sentry, OpenTelemetry, etc.)
|
|
47
|
-
# config.correlation_id_generator = -> {
|
|
48
67
|
# # Example for Datadog APM
|
|
49
68
|
# Datadog::Tracing.active_trace&.id
|
|
50
69
|
#
|
|
@@ -7,7 +7,7 @@ module SolidObserver
|
|
|
7
7
|
# will be configured by the Engine (similar to BaseEvent) when metrics are
|
|
8
8
|
# fully implemented.
|
|
9
9
|
#
|
|
10
|
-
class BaseMetric <
|
|
10
|
+
class BaseMetric < BaseRecord
|
|
11
11
|
self.abstract_class = true
|
|
12
12
|
self.table_name = "solid_observer_metrics"
|
|
13
13
|
|
|
@@ -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 CableEventBuffer
|
|
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::FlushCableEventBuffer
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def log_label
|
|
25
|
+
"Cable buffer"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -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
|
|
@@ -2,52 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
require "singleton"
|
|
4
4
|
|
|
5
|
+
require_relative "event_buffer_core"
|
|
6
|
+
|
|
5
7
|
module SolidObserver
|
|
6
8
|
class CacheEventBuffer
|
|
7
9
|
include Singleton
|
|
10
|
+
include EventBufferCore
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
@mutex = Mutex.new
|
|
11
|
-
@buffer = []
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def push(event_data)
|
|
15
|
-
config = SolidObserver.config
|
|
16
|
-
return unless config.persistence_mode?
|
|
17
|
-
|
|
18
|
-
should_flush = buffer_ready_to_flush?(event_data, config.buffer_size)
|
|
19
|
-
flush! if should_flush
|
|
20
|
-
end
|
|
12
|
+
INITIAL_METRICS = EventBufferCore::INITIAL_METRICS
|
|
21
13
|
|
|
22
|
-
def
|
|
23
|
-
|
|
24
|
-
buffered_events = @buffer.dup
|
|
25
|
-
@buffer.clear
|
|
26
|
-
buffered_events
|
|
27
|
-
end
|
|
28
|
-
return if events.empty?
|
|
29
|
-
|
|
30
|
-
Services::FlushCacheEventBuffer.call(events)
|
|
31
|
-
rescue => error
|
|
32
|
-
@mutex.synchronize { @buffer = events + @buffer }
|
|
33
|
-
Rails.logger&.error("[SolidObserver] Cache buffer flush failed: #{error.message}") if defined?(Rails)
|
|
14
|
+
def initialize
|
|
15
|
+
initialize_event_buffer
|
|
34
16
|
end
|
|
35
17
|
|
|
36
|
-
|
|
37
|
-
@mutex.synchronize { @buffer.size }
|
|
38
|
-
end
|
|
18
|
+
private
|
|
39
19
|
|
|
40
|
-
def
|
|
41
|
-
|
|
20
|
+
def flush_service
|
|
21
|
+
Services::FlushCacheEventBuffer
|
|
42
22
|
end
|
|
43
23
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def buffer_ready_to_flush?(event_data, buffer_size)
|
|
47
|
-
@mutex.synchronize do
|
|
48
|
-
@buffer << event_data
|
|
49
|
-
@buffer.size >= buffer_size
|
|
50
|
-
end
|
|
24
|
+
def log_label
|
|
25
|
+
"Cache buffer"
|
|
51
26
|
end
|
|
52
27
|
end
|
|
53
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
|