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
|
@@ -7,14 +7,15 @@ module SolidObserver
|
|
|
7
7
|
# SQLite uses whole-database page accounting; PostgreSQL and MySQL/Trilogy
|
|
8
8
|
# use table + index size from adapter-native system functions.
|
|
9
9
|
class DatabaseSize
|
|
10
|
-
|
|
10
|
+
DEFAULT_TABLE_NAME = "solid_observer_queue_events"
|
|
11
11
|
|
|
12
|
-
def self.call(connection:)
|
|
13
|
-
new(connection).call
|
|
12
|
+
def self.call(connection:, table_name: DEFAULT_TABLE_NAME)
|
|
13
|
+
new(connection, table_name: table_name).call
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def initialize(connection)
|
|
16
|
+
def initialize(connection, table_name:)
|
|
17
17
|
@connection = connection
|
|
18
|
+
@table_name = table_name
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
def call
|
|
@@ -26,7 +27,7 @@ module SolidObserver
|
|
|
26
27
|
|
|
27
28
|
private
|
|
28
29
|
|
|
29
|
-
attr_reader :connection
|
|
30
|
+
attr_reader :connection, :table_name
|
|
30
31
|
|
|
31
32
|
def adapter_key
|
|
32
33
|
case connection.adapter_name.to_s.downcase
|
|
@@ -52,16 +53,20 @@ module SolidObserver
|
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
def sqlite_size
|
|
55
|
-
connection.query_value("
|
|
56
|
+
page_count = connection.query_value("PRAGMA page_count")
|
|
57
|
+
page_size = connection.query_value("PRAGMA page_size")
|
|
58
|
+
return unless page_count && page_size
|
|
59
|
+
|
|
60
|
+
page_count.to_i * page_size.to_i
|
|
56
61
|
end
|
|
57
62
|
|
|
58
63
|
def postgresql_size
|
|
59
|
-
quoted_table = connection.quote(
|
|
64
|
+
quoted_table = connection.quote(table_name)
|
|
60
65
|
connection.query_value("SELECT pg_total_relation_size(#{quoted_table})")&.to_i
|
|
61
66
|
end
|
|
62
67
|
|
|
63
68
|
def mysql_size
|
|
64
|
-
quoted_table = connection.quote(
|
|
69
|
+
quoted_table = connection.quote(table_name)
|
|
65
70
|
|
|
66
71
|
connection.query_value(<<~SQL)&.to_i
|
|
67
72
|
SELECT COALESCE(data_length + index_length, 0)
|
|
@@ -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,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
module Services
|
|
5
|
+
class FlushCacheEventBuffer
|
|
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
|
+
CacheEvent.transaction { CacheEvent.insert_all!(@events) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def fallback_insert_count(error)
|
|
32
|
+
Rails.logger&.error("[SolidObserver] Cache 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
|
+
CacheEvent.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 cache 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,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
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module SolidObserver
|
|
6
|
+
module Services
|
|
7
|
+
class RecordCacheEvent
|
|
8
|
+
INTERNAL_CACHE_KEY_PREFIX = "solid_observer/"
|
|
9
|
+
|
|
10
|
+
def self.call(event:, buffer:)
|
|
11
|
+
new(event, buffer).call
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(event, buffer)
|
|
15
|
+
@event = event
|
|
16
|
+
@buffer = buffer
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
return if internal_cache_event?
|
|
21
|
+
|
|
22
|
+
record_metric_and_event
|
|
23
|
+
rescue => error
|
|
24
|
+
raise error if error.is_a?(NameError)
|
|
25
|
+
Rails.logger&.warn("[SolidObserver] Cache event recording failed: #{error.message}") if defined?(Rails)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def record_metric_and_event
|
|
31
|
+
SolidObserver::Services::RecordCacheMetric.call(event: @event)
|
|
32
|
+
return unless should_store_event?
|
|
33
|
+
|
|
34
|
+
@buffer.push(build_event_data)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def should_store_event?
|
|
38
|
+
sampled? || slow? || errored?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def sampled?
|
|
42
|
+
rand <= SolidObserver.config.cache_sampling_rate
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def slow?
|
|
46
|
+
duration_in_seconds && duration_in_seconds >= SolidObserver.config.cache_slow_threshold
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def errored?
|
|
50
|
+
return false unless SolidObserver.config.cache_store_errors
|
|
51
|
+
|
|
52
|
+
!exception_data.compact.empty?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def build_event_data
|
|
56
|
+
{
|
|
57
|
+
event_type: cache_operation,
|
|
58
|
+
key_digest: key_digest,
|
|
59
|
+
hit: hit_value,
|
|
60
|
+
duration: duration_in_seconds,
|
|
61
|
+
error_class: exception_data[:error_class],
|
|
62
|
+
error_message: exception_data[:error_message],
|
|
63
|
+
metadata: metadata.to_json,
|
|
64
|
+
recorded_at: Time.current
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def cache_operation
|
|
69
|
+
@event.name.delete_suffix(".active_support")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def duration_in_seconds
|
|
73
|
+
@event.duration&./(1000.0)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def key_digest
|
|
77
|
+
Digest::SHA256.hexdigest(normalized_key_string)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def normalized_key_string
|
|
81
|
+
key = payload[:key]
|
|
82
|
+
|
|
83
|
+
case key
|
|
84
|
+
when Hash
|
|
85
|
+
key.keys.map(&:to_s).sort.join(",")
|
|
86
|
+
when Array
|
|
87
|
+
key.map(&:to_s).sort.join(",")
|
|
88
|
+
else
|
|
89
|
+
key.to_s
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def hit_value
|
|
94
|
+
hit = payload[:hit]
|
|
95
|
+
hits = payload[:hits]
|
|
96
|
+
|
|
97
|
+
return hit unless hit.nil?
|
|
98
|
+
return nil unless hits.is_a?(Array)
|
|
99
|
+
|
|
100
|
+
hits.any?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def metadata
|
|
104
|
+
{
|
|
105
|
+
super_operation: payload[:super_operation]&.to_s,
|
|
106
|
+
key_size: key_size,
|
|
107
|
+
hits_count: hits_count
|
|
108
|
+
}.compact.merge(exception_data).compact
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def key_size
|
|
112
|
+
key = payload[:key]
|
|
113
|
+
return key.keys.size if key.is_a?(Hash)
|
|
114
|
+
return key.size if key.is_a?(Array)
|
|
115
|
+
|
|
116
|
+
key.to_s.bytesize
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def hits_count
|
|
120
|
+
hits = payload[:hits]
|
|
121
|
+
return nil unless hits.is_a?(Array)
|
|
122
|
+
|
|
123
|
+
hits.size
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def exception_data
|
|
127
|
+
@exception_data ||= begin
|
|
128
|
+
exception_obj = payload[:exception_object]
|
|
129
|
+
exception = payload[:exception]
|
|
130
|
+
|
|
131
|
+
if exception_obj
|
|
132
|
+
{error_class: exception_obj.class.name, error_message: exception_obj.message}
|
|
133
|
+
elsif exception.is_a?(Array)
|
|
134
|
+
{error_class: exception.first, error_message: exception.last}
|
|
135
|
+
else
|
|
136
|
+
{}
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def payload
|
|
142
|
+
@event.payload || {}
|
|
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
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../cache_metric_buffer"
|
|
4
|
+
|
|
5
|
+
module SolidObserver
|
|
6
|
+
module Services
|
|
7
|
+
class RecordCacheMetric
|
|
8
|
+
def self.call(event:, buffer: SolidObserver::CacheMetricBuffer.instance)
|
|
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
|
+
@buffer.increment(
|
|
19
|
+
event_type: event_type,
|
|
20
|
+
period_start: period_start,
|
|
21
|
+
operations_count: 1,
|
|
22
|
+
hits_count: hit_increment,
|
|
23
|
+
misses_count: miss_increment,
|
|
24
|
+
errors_count: error_increment,
|
|
25
|
+
duration_total: duration_in_seconds
|
|
26
|
+
)
|
|
27
|
+
rescue => error
|
|
28
|
+
Rails.logger&.warn("[SolidObserver] Cache metric recording failed: #{error.message}") if defined?(Rails)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def period_start
|
|
34
|
+
Time.current.beginning_of_minute
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def event_type
|
|
38
|
+
@event.name.delete_suffix(".active_support")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def hit_increment
|
|
42
|
+
(payload[:hit] == true) ? 1 : 0
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def miss_increment
|
|
46
|
+
(payload[:hit] == false) ? 1 : 0
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def error_increment
|
|
50
|
+
exception_present? ? 1 : 0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def exception_present?
|
|
54
|
+
payload[:exception_object].present? || payload[:exception].is_a?(Array)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def duration_in_seconds
|
|
58
|
+
(@event.duration || 0).to_f / 1000.0
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def payload
|
|
62
|
+
@event.payload || {}
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|