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
|
@@ -123,10 +123,10 @@ module SolidObserver
|
|
|
123
123
|
class EventCounts
|
|
124
124
|
attr_reader :error_count, :slow_count, :latest_recorded_at
|
|
125
125
|
|
|
126
|
-
def initialize
|
|
127
|
-
@error_count =
|
|
128
|
-
@slow_count =
|
|
129
|
-
@latest_recorded_at =
|
|
126
|
+
def initialize(error_count: 0, slow_count: 0, latest_recorded_at: nil)
|
|
127
|
+
@error_count = error_count.to_i
|
|
128
|
+
@slow_count = slow_count.to_i
|
|
129
|
+
@latest_recorded_at = latest_recorded_at
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
def record(recorded_at:, error_class:, duration:)
|
|
@@ -181,22 +181,38 @@ module SolidObserver
|
|
|
181
181
|
attr_reader :window, :current_time
|
|
182
182
|
|
|
183
183
|
def event_counts
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
184
|
+
error_count, slow_count, latest_recorded_at = SolidObserver::CacheEvent.where(recorded_at: window_range).pick(
|
|
185
|
+
Arel.sql("COUNT(CASE WHEN #{error_condition_sql} THEN 1 END)"),
|
|
186
|
+
Arel.sql("COUNT(CASE WHEN #{slow_condition_sql} THEN 1 END)"),
|
|
187
|
+
Arel.sql("MAX(CASE WHEN #{tracked_condition_sql} THEN recorded_at END)")
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
EventCounts.new(
|
|
191
|
+
error_count: error_count,
|
|
192
|
+
slow_count: slow_count,
|
|
193
|
+
latest_recorded_at: latest_recorded_at
|
|
194
|
+
)
|
|
195
195
|
end
|
|
196
196
|
|
|
197
197
|
def window_range
|
|
198
198
|
(current_time - window)..current_time
|
|
199
199
|
end
|
|
200
|
+
|
|
201
|
+
def tracked_condition_sql
|
|
202
|
+
"(#{error_condition_sql}) OR (#{slow_condition_sql})"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def error_condition_sql
|
|
206
|
+
"error_class IS NOT NULL AND TRIM(error_class) != ''"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def slow_condition_sql
|
|
210
|
+
"(error_class IS NULL OR TRIM(error_class) = '') AND duration >= #{slow_threshold}"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def slow_threshold
|
|
214
|
+
SolidObserver.config.cache_slow_threshold.to_f
|
|
215
|
+
end
|
|
200
216
|
end
|
|
201
217
|
|
|
202
218
|
class << self
|
|
@@ -218,7 +234,8 @@ module SolidObserver
|
|
|
218
234
|
current_time = Time.current
|
|
219
235
|
dashboard_response(window: window, current_time: current_time)
|
|
220
236
|
rescue => error
|
|
221
|
-
|
|
237
|
+
Rails.logger&.error("[SolidObserver] CacheStats call failed: #{error.class} #{error.message}") if defined?(Rails)
|
|
238
|
+
error_response
|
|
222
239
|
end
|
|
223
240
|
|
|
224
241
|
private
|
|
@@ -308,7 +325,7 @@ module SolidObserver
|
|
|
308
325
|
numerator.to_f / denominator
|
|
309
326
|
end
|
|
310
327
|
|
|
311
|
-
def error_response
|
|
328
|
+
def error_response
|
|
312
329
|
{
|
|
313
330
|
hit_rate: 0.0,
|
|
314
331
|
throughput: 0.0,
|
|
@@ -321,7 +338,7 @@ module SolidObserver
|
|
|
321
338
|
duration_total: 0.0,
|
|
322
339
|
activity_trends: ACTIVITY_TREND_EMPTY.dup,
|
|
323
340
|
stability: STABILITY_EMPTY.dup,
|
|
324
|
-
error:
|
|
341
|
+
error: "Service temporarily unavailable"
|
|
325
342
|
}
|
|
326
343
|
end
|
|
327
344
|
end
|
|
@@ -6,6 +6,13 @@ require_relative "storage_info_snapshot"
|
|
|
6
6
|
module SolidObserver
|
|
7
7
|
module Services
|
|
8
8
|
class CleanupStorage
|
|
9
|
+
MAINTENANCE_STATEMENT_BUILDERS = {
|
|
10
|
+
"sqlite" => ->(_tables) { ["VACUUM"] },
|
|
11
|
+
"postgresql" => ->(tables) { tables.map { |table_name| "VACUUM ANALYZE #{table_name}" } },
|
|
12
|
+
"mysql2" => ->(tables) { ["OPTIMIZE TABLE #{tables.join(", ")}"] },
|
|
13
|
+
"trilogy" => ->(tables) { ["OPTIMIZE TABLE #{tables.join(", ")}"] }
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
9
16
|
def self.call
|
|
10
17
|
new.call
|
|
11
18
|
end
|
|
@@ -13,37 +20,54 @@ module SolidObserver
|
|
|
13
20
|
def call
|
|
14
21
|
return 0 if SolidObserver.config.realtime_mode?
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
handle_cleanup_failure(e)
|
|
23
|
+
post_cleanup(cleanup_counts)
|
|
24
|
+
rescue => error
|
|
25
|
+
handle_cleanup_failure(error)
|
|
20
26
|
end
|
|
21
27
|
|
|
22
28
|
private
|
|
23
29
|
|
|
30
|
+
def cleanup_counts
|
|
31
|
+
perform_cleanup.tap { record_snapshot_after_cleanup }
|
|
32
|
+
end
|
|
33
|
+
|
|
24
34
|
def handle_cleanup_failure(error)
|
|
25
35
|
Rails.logger.error "[SolidObserver] Cleanup failed: #{error.message}"
|
|
26
36
|
raise
|
|
27
37
|
end
|
|
28
38
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
def perform_cleanup
|
|
40
|
+
config = SolidObserver.config
|
|
41
|
+
event_cutoff = config.event_retention.ago
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
queue_events: QueueEvent.transaction do
|
|
45
|
+
QueueEvent.where("recorded_at < ?", event_cutoff).delete_all
|
|
46
|
+
end,
|
|
47
|
+
cache_events: delete_telemetry_records(
|
|
48
|
+
SolidObserver::CacheEvent,
|
|
49
|
+
column: :recorded_at,
|
|
50
|
+
cutoff: event_cutoff
|
|
51
|
+
),
|
|
52
|
+
cache_metrics: delete_telemetry_records(
|
|
53
|
+
SolidObserver::CacheMetric,
|
|
54
|
+
column: :period_start,
|
|
55
|
+
cutoff: config.metrics_retention.ago
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def post_cleanup(cleanup_counts)
|
|
38
61
|
vacuum_database
|
|
39
62
|
check_storage_warnings
|
|
40
|
-
log_results(
|
|
41
|
-
|
|
63
|
+
log_results(cleanup_counts)
|
|
64
|
+
cleanup_counts.values.sum
|
|
42
65
|
end
|
|
43
66
|
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
67
|
+
def delete_telemetry_records(model, column:, cutoff:)
|
|
68
|
+
return 0 unless data_source_available?(model)
|
|
69
|
+
|
|
70
|
+
model.where("#{column} < ?", cutoff).delete_all
|
|
47
71
|
end
|
|
48
72
|
|
|
49
73
|
def record_snapshot_after_cleanup
|
|
@@ -70,58 +94,69 @@ module SolidObserver
|
|
|
70
94
|
end
|
|
71
95
|
|
|
72
96
|
def vacuum_database
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
Rails.logger.warn "[SolidObserver] Database maintenance failed: #{e.message}"
|
|
97
|
+
maintenance_statements.each do |statement|
|
|
98
|
+
QueueEvent.connection.execute(statement)
|
|
99
|
+
end
|
|
100
|
+
rescue => error
|
|
101
|
+
Rails.logger.warn "[SolidObserver] Database maintenance failed: #{error.message}"
|
|
79
102
|
end
|
|
80
103
|
|
|
81
104
|
def check_storage_warnings
|
|
82
105
|
current_size = current_database_size
|
|
83
|
-
return unless
|
|
106
|
+
return unless current_size
|
|
107
|
+
return unless current_size > (SolidObserver.config.max_db_size * SolidObserver.config.warning_threshold)
|
|
84
108
|
|
|
85
109
|
Rails.logger.warn(storage_warning_message(current_size))
|
|
86
110
|
end
|
|
87
111
|
|
|
88
|
-
def warning_needed?(current_size)
|
|
89
|
-
return false unless current_size
|
|
90
|
-
|
|
91
|
-
config = SolidObserver.config
|
|
92
|
-
max_size = config.max_db_size
|
|
93
|
-
threshold = config.warning_threshold
|
|
94
|
-
current_size > (max_size * threshold)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
112
|
def storage_warning_message(current_size)
|
|
98
113
|
max_size = SolidObserver.config.max_db_size
|
|
99
114
|
percentage = ((current_size.to_f / max_size) * 100).round(1)
|
|
100
|
-
current_size_human =
|
|
101
|
-
|
|
115
|
+
current_size_human = ActiveSupport::NumberHelper.number_to_human_size(
|
|
116
|
+
current_size,
|
|
117
|
+
precision: 1,
|
|
118
|
+
significant: false,
|
|
119
|
+
strip_insignificant_zeros: false
|
|
120
|
+
)
|
|
121
|
+
max_size_human = ActiveSupport::NumberHelper.number_to_human_size(
|
|
122
|
+
max_size,
|
|
123
|
+
precision: 1,
|
|
124
|
+
significant: false,
|
|
125
|
+
strip_insignificant_zeros: false
|
|
126
|
+
)
|
|
102
127
|
"[SolidObserver] Queue DB approaching limit: #{current_size_human} / #{max_size_human} (#{percentage}%)"
|
|
103
128
|
end
|
|
104
129
|
|
|
105
|
-
def human_size(bytes)
|
|
106
|
-
ActiveSupport::NumberHelper.number_to_human_size(bytes, precision: 1, significant: false, strip_insignificant_zeros: false)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
130
|
def current_database_size
|
|
110
131
|
return @current_database_size if defined?(@current_database_size)
|
|
111
132
|
|
|
112
133
|
@current_database_size = DatabaseSize.call(connection: QueueEvent.connection)
|
|
113
134
|
end
|
|
114
135
|
|
|
115
|
-
def log_results(
|
|
116
|
-
Rails.logger.info
|
|
136
|
+
def log_results(cleanup_counts)
|
|
137
|
+
Rails.logger.info(
|
|
138
|
+
"[SolidObserver] Cleaned #{cleanup_counts[:queue_events]} queue events, " \
|
|
139
|
+
"#{cleanup_counts[:cache_events]} cache events, " \
|
|
140
|
+
"#{cleanup_counts[:cache_metrics]} cache metrics"
|
|
141
|
+
)
|
|
117
142
|
end
|
|
118
143
|
|
|
119
|
-
def
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
when "postgresql" then "VACUUM ANALYZE solid_observer_queue_events"
|
|
123
|
-
when "mysql2", "trilogy" then "OPTIMIZE TABLE solid_observer_queue_events"
|
|
144
|
+
def maintenance_statements
|
|
145
|
+
tables = [QueueEvent, SolidObserver::CacheEvent, SolidObserver::CacheMetric].filter_map do |model|
|
|
146
|
+
model.table_name if data_source_available?(model)
|
|
124
147
|
end
|
|
148
|
+
return [] if tables.empty?
|
|
149
|
+
|
|
150
|
+
MAINTENANCE_STATEMENT_BUILDERS.fetch(QueueEvent.connection.adapter_name.downcase, ->(_known_tables) { [] }).call(tables)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def data_source_available?(model)
|
|
154
|
+
table_name = model.table_name.to_s
|
|
155
|
+
return false if table_name.empty?
|
|
156
|
+
|
|
157
|
+
model.connection.data_source_exists?(table_name)
|
|
158
|
+
rescue *StorageInfoSnapshot::CONNECTION_ERRORS, TypeError
|
|
159
|
+
false
|
|
125
160
|
end
|
|
126
161
|
end
|
|
127
162
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
module Services
|
|
5
|
+
class FlushCableEventBuffer
|
|
6
|
+
BATCH_SIZE = 100
|
|
7
|
+
|
|
8
|
+
def self.call(events)
|
|
9
|
+
new(events).call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(events)
|
|
13
|
+
@events = events
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
return 0 if @events.empty?
|
|
18
|
+
|
|
19
|
+
insert_all_events
|
|
20
|
+
@events.size
|
|
21
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::StatementInvalid => error
|
|
22
|
+
fallback_insert_count(error)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def insert_all_events
|
|
28
|
+
CableEvent.transaction { CableEvent.insert_all!(@events) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def fallback_insert_count(error)
|
|
32
|
+
Rails.logger&.error("[SolidObserver] Cable bulk insert failed, retrying in batches: #{error.message}") if defined?(Rails)
|
|
33
|
+
@events.each_slice(BATCH_SIZE).sum { |batch| insert_batch(batch) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def insert_batch(batch)
|
|
37
|
+
size = batch.size
|
|
38
|
+
CableEvent.insert_all(batch, returning: false)
|
|
39
|
+
size
|
|
40
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::StatementInvalid => error
|
|
41
|
+
handle_batch_insert_error(size, error)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def log_batch_warning(size, error)
|
|
45
|
+
Rails.logger&.warn("[SolidObserver] Failed to insert cable batch of #{size} events: #{error.message}") if defined?(Rails)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def handle_batch_insert_error(size, error)
|
|
49
|
+
log_batch_warning(size, error)
|
|
50
|
+
0
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
module Services
|
|
5
|
+
class FlushCableMetrics
|
|
6
|
+
def self.call(metrics)
|
|
7
|
+
new(metrics).call
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(metrics)
|
|
11
|
+
@metrics = metrics
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
return 0 if @metrics.empty?
|
|
16
|
+
|
|
17
|
+
flush_metrics
|
|
18
|
+
@metrics.size
|
|
19
|
+
rescue ActiveRecord::RecordNotUnique
|
|
20
|
+
retry
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def flush_metrics
|
|
26
|
+
SolidObserver::CableMetric.transaction do
|
|
27
|
+
@metrics.each { |metric_data| increment_metric(metric_data) }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def increment_metric(metric_data)
|
|
32
|
+
period_start = metric_data.fetch(:period_start)
|
|
33
|
+
metric = SolidObserver::CableMetric.find_or_create_by!(period_start: period_start)
|
|
34
|
+
|
|
35
|
+
SolidObserver::CableMetric.where(id: metric.id).update_counters(update_values(metric_data))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def update_values(metric_data)
|
|
39
|
+
{
|
|
40
|
+
broadcasts_count: increment_expression(:broadcasts_count, metric_data),
|
|
41
|
+
transmissions_count: increment_expression(:transmissions_count, metric_data),
|
|
42
|
+
confirmations_count: increment_expression(:confirmations_count, metric_data),
|
|
43
|
+
rejections_count: increment_expression(:rejections_count, metric_data),
|
|
44
|
+
perform_actions_count: increment_expression(:perform_actions_count, metric_data),
|
|
45
|
+
errors_count: increment_expression(:errors_count, metric_data)
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def increment_expression(column, metric_data)
|
|
50
|
+
metric_data.fetch(column, 0)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
module Services
|
|
5
|
+
class FlushCacheMetrics
|
|
6
|
+
def self.call(metrics)
|
|
7
|
+
new(metrics).call
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(metrics)
|
|
11
|
+
@metrics = metrics
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
return 0 if @metrics.empty?
|
|
16
|
+
|
|
17
|
+
flush_metrics
|
|
18
|
+
@metrics.size
|
|
19
|
+
rescue ActiveRecord::RecordNotUnique
|
|
20
|
+
retry
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def flush_metrics
|
|
26
|
+
SolidObserver::CacheMetric.transaction do
|
|
27
|
+
@metrics.each { |metric_data| increment_metric(metric_data) }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def increment_metric(metric_data)
|
|
32
|
+
event_type, period_start = metric_data.values_at(:event_type, :period_start)
|
|
33
|
+
metric = SolidObserver::CacheMetric.find_or_create_by!(
|
|
34
|
+
event_type: event_type,
|
|
35
|
+
period_start: period_start
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
SolidObserver::CacheMetric.where(id: metric.id).update_counters(update_values(metric_data))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def update_values(metric_data)
|
|
42
|
+
{
|
|
43
|
+
operations_count: increment_expression(:operations_count, metric_data),
|
|
44
|
+
hits_count: increment_expression(:hits_count, metric_data),
|
|
45
|
+
misses_count: increment_expression(:misses_count, metric_data),
|
|
46
|
+
errors_count: increment_expression(:errors_count, metric_data),
|
|
47
|
+
duration_total: increment_expression(:duration_total, metric_data)
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def increment_expression(column, metric_data)
|
|
52
|
+
metric_data.fetch(column, 0)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module SolidObserver
|
|
6
|
+
module Services
|
|
7
|
+
class RecordCableEvent
|
|
8
|
+
def self.call(event:, buffer:)
|
|
9
|
+
new(event, buffer).call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(event, buffer)
|
|
13
|
+
@event = event
|
|
14
|
+
@buffer = buffer
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
record_metric_and_event
|
|
19
|
+
rescue => error
|
|
20
|
+
raise error if error.is_a?(NameError)
|
|
21
|
+
Rails.logger&.warn("[SolidObserver] Cable event recording failed: #{error.message}") if defined?(Rails)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def record_metric_and_event
|
|
27
|
+
SolidObserver::Services::RecordCableMetric.call(event: @event)
|
|
28
|
+
return unless should_store_event?
|
|
29
|
+
|
|
30
|
+
@buffer.push(build_event_data)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def should_store_event?
|
|
34
|
+
case @event.name
|
|
35
|
+
when "broadcast.action_cable"
|
|
36
|
+
sampled? || errored?
|
|
37
|
+
when "transmit_subscription_rejection.action_cable"
|
|
38
|
+
true
|
|
39
|
+
else
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def sampled?
|
|
45
|
+
rand <= SolidObserver.config.cable_sampling_rate
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def errored?
|
|
49
|
+
!exception_data.compact.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def build_event_data
|
|
53
|
+
{
|
|
54
|
+
event_type: @event.name.delete_suffix(".action_cable"),
|
|
55
|
+
channel_class: channel_class,
|
|
56
|
+
broadcasting_digest: broadcasting_digest,
|
|
57
|
+
duration: duration_in_seconds,
|
|
58
|
+
error_class: exception_data[:error_class],
|
|
59
|
+
error_message: exception_data[:error_message],
|
|
60
|
+
metadata: metadata.to_json,
|
|
61
|
+
recorded_at: Time.current
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def broadcasting_digest
|
|
66
|
+
return nil unless @event.name == "broadcast.action_cable"
|
|
67
|
+
|
|
68
|
+
Digest::SHA256.hexdigest(payload[:broadcasting].to_s)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def channel_class
|
|
72
|
+
class_name = payload[:channel_class]
|
|
73
|
+
return class_name if class_name
|
|
74
|
+
|
|
75
|
+
channel = payload[:channel]
|
|
76
|
+
return nil unless channel
|
|
77
|
+
|
|
78
|
+
channel.class.name || channel.to_s
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def duration_in_seconds
|
|
82
|
+
@event.duration&./(1000.0)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def metadata
|
|
86
|
+
{
|
|
87
|
+
action: payload[:action]&.to_s,
|
|
88
|
+
via: payload[:via]&.to_s,
|
|
89
|
+
data_size: payload[:data]&.to_s&.bytesize,
|
|
90
|
+
message_size: payload[:message]&.to_s&.bytesize
|
|
91
|
+
}.compact
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def exception_data
|
|
95
|
+
@exception_data ||= begin
|
|
96
|
+
exception_obj = payload[:exception_object]
|
|
97
|
+
exception = payload[:exception]
|
|
98
|
+
|
|
99
|
+
if exception_obj
|
|
100
|
+
{error_class: exception_obj.class.name, error_message: exception_obj.message}
|
|
101
|
+
elsif exception.is_a?(Array)
|
|
102
|
+
{error_class: exception.first, error_message: exception.last}
|
|
103
|
+
else
|
|
104
|
+
{}
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def payload
|
|
110
|
+
@event.payload || {}
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../cable_metric_buffer"
|
|
4
|
+
|
|
5
|
+
module SolidObserver
|
|
6
|
+
module Services
|
|
7
|
+
class RecordCableMetric
|
|
8
|
+
COUNTERS = %i[
|
|
9
|
+
broadcasts_count
|
|
10
|
+
transmissions_count
|
|
11
|
+
confirmations_count
|
|
12
|
+
rejections_count
|
|
13
|
+
perform_actions_count
|
|
14
|
+
errors_count
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
EVENT_COUNTER_MAP = {
|
|
18
|
+
"broadcast.action_cable" => :broadcasts_count,
|
|
19
|
+
"transmit.action_cable" => :transmissions_count,
|
|
20
|
+
"transmit_subscription_confirmation.action_cable" => :confirmations_count,
|
|
21
|
+
"transmit_subscription_rejection.action_cable" => :rejections_count,
|
|
22
|
+
"perform_action.action_cable" => :perform_actions_count
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
def self.call(event:, buffer: SolidObserver::CableMetricBuffer.instance)
|
|
26
|
+
new(event, buffer).call
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(event, buffer)
|
|
30
|
+
@event = event
|
|
31
|
+
@buffer = buffer
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def call
|
|
35
|
+
@buffer.increment(metric_data)
|
|
36
|
+
rescue => error
|
|
37
|
+
Rails.logger&.warn("[SolidObserver] Cable metric recording failed: #{error.message}") if defined?(Rails)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def metric_data
|
|
43
|
+
COUNTERS.each_with_object({period_start: period_start}) do |counter, data|
|
|
44
|
+
data[counter] = (counter == target_counter) ? 1 : 0
|
|
45
|
+
end.merge(errors_count: error_increment)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def target_counter
|
|
49
|
+
EVENT_COUNTER_MAP.fetch(event_name, :broadcasts_count)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def period_start
|
|
53
|
+
Time.current.beginning_of_minute
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def event_name
|
|
57
|
+
@event.name
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def error_increment
|
|
61
|
+
exception_present? ? 1 : 0
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def exception_present?
|
|
65
|
+
payload[:exception_object].present? || payload[:exception].is_a?(Array)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def payload
|
|
69
|
+
@event.payload || {}
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -5,6 +5,8 @@ require "digest"
|
|
|
5
5
|
module SolidObserver
|
|
6
6
|
module Services
|
|
7
7
|
class RecordCacheEvent
|
|
8
|
+
INTERNAL_CACHE_KEY_PREFIX = "solid_observer/"
|
|
9
|
+
|
|
8
10
|
def self.call(event:, buffer:)
|
|
9
11
|
new(event, buffer).call
|
|
10
12
|
end
|
|
@@ -15,6 +17,8 @@ module SolidObserver
|
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def call
|
|
20
|
+
return if internal_cache_event?
|
|
21
|
+
|
|
18
22
|
record_metric_and_event
|
|
19
23
|
rescue => error
|
|
20
24
|
raise error if error.is_a?(NameError)
|
|
@@ -137,6 +141,25 @@ module SolidObserver
|
|
|
137
141
|
def payload
|
|
138
142
|
@event.payload || {}
|
|
139
143
|
end
|
|
144
|
+
|
|
145
|
+
def internal_cache_event?
|
|
146
|
+
internal_cache_key?(payload[:key])
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def internal_cache_key?(key)
|
|
150
|
+
case key
|
|
151
|
+
when Array
|
|
152
|
+
nested_internal_cache_key?(key)
|
|
153
|
+
when Hash
|
|
154
|
+
nested_internal_cache_key?(key.flatten(1))
|
|
155
|
+
else
|
|
156
|
+
key.to_s.start_with?(INTERNAL_CACHE_KEY_PREFIX)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def nested_internal_cache_key?(entries)
|
|
161
|
+
entries.any? { |entry| internal_cache_key?(entry) }
|
|
162
|
+
end
|
|
140
163
|
end
|
|
141
164
|
end
|
|
142
165
|
end
|