solid_observer 0.3.0 → 0.4.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/README.md +134 -81
  4. data/app/assets/stylesheets/solid_observer/application.css +18 -0
  5. data/app/controllers/solid_observer/cache_dashboard_controller.rb +59 -0
  6. data/app/controllers/solid_observer/cache_operations_controller.rb +24 -0
  7. data/app/controllers/solid_observer/dashboard_controller.rb +44 -1
  8. data/app/controllers/solid_observer/storages_controller.rb +1 -1
  9. data/app/helpers/solid_observer/application_helper.rb +154 -5
  10. data/app/helpers/solid_observer/dashboard_helper.rb +30 -11
  11. data/app/models/solid_observer/cache_event.rb +15 -0
  12. data/app/models/solid_observer/cache_metric.rb +14 -0
  13. data/app/models/solid_observer/storage_info.rb +4 -1
  14. data/app/views/layouts/solid_observer/application.html.erb +144 -17
  15. data/app/views/solid_observer/cache_dashboard/_charts.html.erb +40 -0
  16. data/app/views/solid_observer/cache_dashboard/_recent_events.html.erb +34 -0
  17. data/app/views/solid_observer/cache_dashboard/_summary.html.erb +39 -0
  18. data/app/views/solid_observer/cache_dashboard/index.html.erb +62 -0
  19. data/app/views/solid_observer/cache_operations/_confirm_clear.html.erb +6 -0
  20. data/app/views/solid_observer/cache_operations/index.html.erb +60 -0
  21. data/app/views/solid_observer/dashboard/index.html.erb +34 -4
  22. data/app/views/solid_observer/jobs/show.html.erb +3 -3
  23. data/app/views/solid_observer/storages/show.html.erb +64 -32
  24. data/bin/quality_gate +1 -1
  25. data/config/routes.rb +5 -0
  26. data/db/migrate/20260601000001_create_solid_observer_cache_events.rb +22 -0
  27. data/db/migrate/20260601000002_create_solid_observer_cache_metrics.rb +18 -0
  28. data/db/migrate/20260602000001_add_component_to_solid_observer_storage_infos.rb +8 -0
  29. data/lib/generators/solid_observer/templates/initializer.rb.tt +2 -1
  30. data/lib/solid_observer/cache_event_buffer.rb +53 -0
  31. data/lib/solid_observer/cache_subscriber.rb +47 -0
  32. data/lib/solid_observer/cli/storage.rb +16 -13
  33. data/lib/solid_observer/configuration.rb +22 -3
  34. data/lib/solid_observer/engine.rb +44 -7
  35. data/lib/solid_observer/services/cache_operations.rb +115 -0
  36. data/lib/solid_observer/services/cache_stats.rb +329 -0
  37. data/lib/solid_observer/services/cleanup_storage.rb +18 -2
  38. data/lib/solid_observer/services/database_size.rb +13 -8
  39. data/lib/solid_observer/services/flush_cache_event_buffer.rb +54 -0
  40. data/lib/solid_observer/services/record_cache_event.rb +142 -0
  41. data/lib/solid_observer/services/record_cache_metric.rb +74 -0
  42. data/lib/solid_observer/services/storage_info_snapshot.rb +128 -0
  43. data/lib/solid_observer/version.rb +1 -1
  44. data/lib/tasks/solid_observer.rake +29 -0
  45. metadata +23 -1
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module SolidObserver
6
+ module Services
7
+ class RecordCacheEvent
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] Cache event recording failed: #{error.message}") if defined?(Rails)
22
+ end
23
+
24
+ private
25
+
26
+ def record_metric_and_event
27
+ SolidObserver::Services::RecordCacheMetric.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
+ sampled? || slow? || errored?
35
+ end
36
+
37
+ def sampled?
38
+ rand <= SolidObserver.config.cache_sampling_rate
39
+ end
40
+
41
+ def slow?
42
+ duration_in_seconds && duration_in_seconds >= SolidObserver.config.cache_slow_threshold
43
+ end
44
+
45
+ def errored?
46
+ return false unless SolidObserver.config.cache_store_errors
47
+
48
+ !exception_data.compact.empty?
49
+ end
50
+
51
+ def build_event_data
52
+ {
53
+ event_type: cache_operation,
54
+ key_digest: key_digest,
55
+ hit: hit_value,
56
+ duration: duration_in_seconds,
57
+ error_class: exception_data[:error_class],
58
+ error_message: exception_data[:error_message],
59
+ metadata: metadata.to_json,
60
+ recorded_at: Time.current
61
+ }
62
+ end
63
+
64
+ def cache_operation
65
+ @event.name.delete_suffix(".active_support")
66
+ end
67
+
68
+ def duration_in_seconds
69
+ @event.duration&./(1000.0)
70
+ end
71
+
72
+ def key_digest
73
+ Digest::SHA256.hexdigest(normalized_key_string)
74
+ end
75
+
76
+ def normalized_key_string
77
+ key = payload[:key]
78
+
79
+ case key
80
+ when Hash
81
+ key.keys.map(&:to_s).sort.join(",")
82
+ when Array
83
+ key.map(&:to_s).sort.join(",")
84
+ else
85
+ key.to_s
86
+ end
87
+ end
88
+
89
+ def hit_value
90
+ hit = payload[:hit]
91
+ hits = payload[:hits]
92
+
93
+ return hit unless hit.nil?
94
+ return nil unless hits.is_a?(Array)
95
+
96
+ hits.any?
97
+ end
98
+
99
+ def metadata
100
+ {
101
+ super_operation: payload[:super_operation]&.to_s,
102
+ key_size: key_size,
103
+ hits_count: hits_count
104
+ }.compact.merge(exception_data).compact
105
+ end
106
+
107
+ def key_size
108
+ key = payload[:key]
109
+ return key.keys.size if key.is_a?(Hash)
110
+ return key.size if key.is_a?(Array)
111
+
112
+ key.to_s.bytesize
113
+ end
114
+
115
+ def hits_count
116
+ hits = payload[:hits]
117
+ return nil unless hits.is_a?(Array)
118
+
119
+ hits.size
120
+ end
121
+
122
+ def exception_data
123
+ @exception_data ||= begin
124
+ exception_obj = payload[:exception_object]
125
+ exception = payload[:exception]
126
+
127
+ if exception_obj
128
+ {error_class: exception_obj.class.name, error_message: exception_obj.message}
129
+ elsif exception.is_a?(Array)
130
+ {error_class: exception.first, error_message: exception.last}
131
+ else
132
+ {}
133
+ end
134
+ end
135
+ end
136
+
137
+ def payload
138
+ @event.payload || {}
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidObserver
4
+ module Services
5
+ class RecordCacheMetric
6
+ def self.call(event:)
7
+ new(event).call
8
+ end
9
+
10
+ def initialize(event)
11
+ @event = event
12
+ end
13
+
14
+ def call
15
+ metric = SolidObserver::CacheMetric.find_or_create_by!(
16
+ event_type: event_type,
17
+ period_start: period_start
18
+ )
19
+
20
+ SolidObserver::CacheMetric
21
+ .where(id: metric.id)
22
+ .update_all(update_values)
23
+ rescue ActiveRecord::RecordNotUnique
24
+ retry
25
+ rescue => error
26
+ Rails.logger&.warn("[SolidObserver] Cache metric recording failed: #{error.message}") if defined?(Rails)
27
+ end
28
+
29
+ private
30
+
31
+ def period_start
32
+ Time.current.beginning_of_minute
33
+ end
34
+
35
+ def event_type
36
+ @event.name.delete_suffix(".active_support")
37
+ end
38
+
39
+ def update_values
40
+ {
41
+ operations_count: Arel.sql("operations_count + 1"),
42
+ hits_count: Arel.sql("hits_count + #{hit_increment}"),
43
+ misses_count: Arel.sql("misses_count + #{miss_increment}"),
44
+ errors_count: Arel.sql("errors_count + #{error_increment}"),
45
+ duration_total: Arel.sql("duration_total + #{duration_in_seconds}")
46
+ }
47
+ end
48
+
49
+ def hit_increment
50
+ (payload[:hit] == true) ? 1 : 0
51
+ end
52
+
53
+ def miss_increment
54
+ (payload[:hit] == false) ? 1 : 0
55
+ end
56
+
57
+ def error_increment
58
+ exception_present? ? 1 : 0
59
+ end
60
+
61
+ def exception_present?
62
+ payload[:exception_object].present? || payload[:exception].is_a?(Array)
63
+ end
64
+
65
+ def duration_in_seconds
66
+ (@event.duration || 0).to_f / 1000.0
67
+ end
68
+
69
+ def payload
70
+ @event.payload || {}
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "database_size"
4
+
5
+ module SolidObserver
6
+ module Services
7
+ class StorageInfoSnapshot
8
+ Component = Struct.new(:key, :label, :record_label, :model, :enabled, keyword_init: true) do
9
+ def enabled?
10
+ enabled.call
11
+ end
12
+
13
+ def solid_cache?
14
+ key == "solid_cache"
15
+ end
16
+
17
+ def storage_model
18
+ model.call
19
+ end
20
+
21
+ def data_source_exists?(connection, table_name)
22
+ connection.data_source_exists?(table_name)
23
+ end
24
+
25
+ def database_size(connection, table_name)
26
+ DatabaseSize.call(connection: connection, table_name: table_name)
27
+ end
28
+
29
+ def snapshot
30
+ return unless enabled?
31
+ return unavailable_snapshot(reason: "SolidCache is unavailable") if unavailable_solid_cache?
32
+
33
+ existing_data_source_snapshot
34
+ rescue *StorageInfoSnapshot::CONNECTION_ERRORS, TypeError
35
+ unavailable_snapshot(reason: "Storage unavailable")
36
+ end
37
+
38
+ def available_snapshot(db_size_bytes:, event_count:)
39
+ {
40
+ component: key,
41
+ label: label,
42
+ available: true,
43
+ db_size_bytes: db_size_bytes,
44
+ event_count: event_count,
45
+ record_label: record_label,
46
+ recorded_at: Time.current,
47
+ unavailable_reason: nil
48
+ }
49
+ end
50
+
51
+ def unavailable_snapshot(reason:)
52
+ {
53
+ component: key,
54
+ label: label,
55
+ available: false,
56
+ db_size_bytes: nil,
57
+ event_count: nil,
58
+ record_label: record_label,
59
+ recorded_at: nil,
60
+ unavailable_reason: reason
61
+ }
62
+ end
63
+
64
+ private
65
+
66
+ def unavailable_solid_cache?
67
+ solid_cache? && !defined?(::SolidCache::Entry)
68
+ end
69
+
70
+ def existing_data_source_snapshot
71
+ record_model = storage_model
72
+ connection = record_model.connection
73
+ table_name = record_model.table_name.to_s
74
+ return table_unavailable_snapshot if table_name.empty? || !data_source_exists?(connection, table_name)
75
+
76
+ available_snapshot(
77
+ db_size_bytes: database_size(connection, table_name),
78
+ event_count: record_model.count
79
+ )
80
+ end
81
+
82
+ def table_unavailable_snapshot
83
+ unavailable_snapshot(reason: "Table unavailable")
84
+ end
85
+ end
86
+
87
+ COMPONENTS = [
88
+ Component.new(
89
+ key: "queue_observer",
90
+ label: "Queue observer",
91
+ record_label: "observer events",
92
+ model: -> { SolidObserver::QueueEvent },
93
+ enabled: -> { SolidObserver.config.solid_queue_enabled? }
94
+ ),
95
+ Component.new(
96
+ key: "cache_observer",
97
+ label: "Cache observer",
98
+ record_label: "observer events",
99
+ model: -> { SolidObserver::CacheEvent },
100
+ enabled: -> { SolidObserver.config.solid_cache_enabled? }
101
+ ),
102
+ Component.new(
103
+ key: "solid_cache",
104
+ label: "SolidCache",
105
+ record_label: "cache rows",
106
+ model: -> { ::SolidCache::Entry },
107
+ enabled: -> { SolidObserver.config.solid_cache_enabled? }
108
+ )
109
+ ].freeze
110
+
111
+ CONNECTION_ERRORS = [
112
+ ActiveRecord::ConnectionNotEstablished,
113
+ ActiveRecord::StatementInvalid,
114
+ *([PG::ConnectionBad] if defined?(PG::ConnectionBad)),
115
+ *([Mysql2::Error::ConnectionError] if defined?(Mysql2::Error::ConnectionError)),
116
+ *([SQLite3::CantOpenException] if defined?(SQLite3::CantOpenException))
117
+ ].freeze
118
+
119
+ def self.call
120
+ new.call
121
+ end
122
+
123
+ def call
124
+ COMPONENTS.filter_map(&:snapshot)
125
+ end
126
+ end
127
+ end
128
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidObserver
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  RUBY_MINIMUM_VERSION = "3.2.0"
6
6
  RAILS_MINIMUM_VERSION = "8.0"
7
7
  end
@@ -68,6 +68,35 @@ namespace :solid_observer do
68
68
  end
69
69
  end
70
70
 
71
+ namespace :cache do
72
+ desc "Clear all SolidCache entries after confirmation"
73
+ task clear: :environment do
74
+ if !SolidObserver::Services::CacheOperations.available?
75
+ puts SolidObserver::Services::CacheOperations.unavailable_message
76
+ next
77
+ end
78
+
79
+ print "#{SolidObserver::Services::CacheOperations.message(:clear, :confirmation)} (y/N) "
80
+ $stdout.flush
81
+
82
+ if $stdin.gets&.strip&.downcase == "y"
83
+ puts SolidObserver::Services::CacheOperations.clear[:message]
84
+ else
85
+ puts "Aborted"
86
+ end
87
+ end
88
+
89
+ desc "Prune expired SolidCache entries"
90
+ task prune: :environment do
91
+ if !SolidObserver::Services::CacheOperations.available?
92
+ puts SolidObserver::Services::CacheOperations.unavailable_message
93
+ next
94
+ end
95
+
96
+ puts SolidObserver::Services::CacheOperations.prune[:message]
97
+ end
98
+ end
99
+
71
100
  namespace :storage do
72
101
  desc "Run storage cleanup based on retention policy"
73
102
  task cleanup: :environment do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_observer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - BartOz
@@ -50,10 +50,13 @@ files:
50
50
  - LICENSE.txt
51
51
  - README.md
52
52
  - app/assets/javascripts/solid_observer/live_poll.js
53
+ - app/assets/stylesheets/solid_observer/application.css
53
54
  - app/controllers/concerns/solid_observer/paginatable.rb
54
55
  - app/controllers/concerns/solid_observer/require_persistence_mode.rb
55
56
  - app/controllers/concerns/solid_observer/require_solid_queue.rb
56
57
  - app/controllers/solid_observer/application_controller.rb
58
+ - app/controllers/solid_observer/cache_dashboard_controller.rb
59
+ - app/controllers/solid_observer/cache_operations_controller.rb
57
60
  - app/controllers/solid_observer/dashboard_controller.rb
58
61
  - app/controllers/solid_observer/events_controller.rb
59
62
  - app/controllers/solid_observer/jobs_controller.rb
@@ -61,11 +64,19 @@ files:
61
64
  - app/helpers/solid_observer/application_helper.rb
62
65
  - app/helpers/solid_observer/dashboard_helper.rb
63
66
  - app/jobs/solid_observer/cleanup_job.rb
67
+ - app/models/solid_observer/cache_event.rb
68
+ - app/models/solid_observer/cache_metric.rb
64
69
  - app/models/solid_observer/queue_event.rb
65
70
  - app/models/solid_observer/queue_metric.rb
66
71
  - app/models/solid_observer/storage_info.rb
67
72
  - app/presenters/solid_observer/execution_presenter.rb
68
73
  - app/views/layouts/solid_observer/application.html.erb
74
+ - app/views/solid_observer/cache_dashboard/_charts.html.erb
75
+ - app/views/solid_observer/cache_dashboard/_recent_events.html.erb
76
+ - app/views/solid_observer/cache_dashboard/_summary.html.erb
77
+ - app/views/solid_observer/cache_dashboard/index.html.erb
78
+ - app/views/solid_observer/cache_operations/_confirm_clear.html.erb
79
+ - app/views/solid_observer/cache_operations/index.html.erb
69
80
  - app/views/solid_observer/dashboard/_chart.html.erb
70
81
  - app/views/solid_observer/dashboard/_live_state.html.erb
71
82
  - app/views/solid_observer/dashboard/_queue_table.html.erb
@@ -89,11 +100,16 @@ files:
89
100
  - db/migrate/20260115000002_create_solid_observer_metrics.rb
90
101
  - db/migrate/20260115000003_create_solid_observer_storage_info.rb
91
102
  - db/migrate/20260424000001_add_composite_indexes_to_queue_events.rb
103
+ - db/migrate/20260601000001_create_solid_observer_cache_events.rb
104
+ - db/migrate/20260601000002_create_solid_observer_cache_metrics.rb
105
+ - db/migrate/20260602000001_add_component_to_solid_observer_storage_infos.rb
92
106
  - lib/generators/solid_observer/install_generator.rb
93
107
  - lib/generators/solid_observer/templates/initializer.rb.tt
94
108
  - lib/solid_observer.rb
95
109
  - lib/solid_observer/base_event.rb
96
110
  - lib/solid_observer/base_metric.rb
111
+ - lib/solid_observer/cache_event_buffer.rb
112
+ - lib/solid_observer/cache_subscriber.rb
97
113
  - lib/solid_observer/chart_buffer.rb
98
114
  - lib/solid_observer/cli/base.rb
99
115
  - lib/solid_observer/cli/jobs.rb
@@ -109,11 +125,17 @@ files:
109
125
  - lib/solid_observer/queries/job_executions_query.rb
110
126
  - lib/solid_observer/queue_event_buffer.rb
111
127
  - lib/solid_observer/queue_stats.rb
128
+ - lib/solid_observer/services/cache_operations.rb
129
+ - lib/solid_observer/services/cache_stats.rb
112
130
  - lib/solid_observer/services/cleanup_storage.rb
113
131
  - lib/solid_observer/services/database_size.rb
132
+ - lib/solid_observer/services/flush_cache_event_buffer.rb
114
133
  - lib/solid_observer/services/flush_event_buffer.rb
115
134
  - lib/solid_observer/services/install_migrations.rb
135
+ - lib/solid_observer/services/record_cache_event.rb
136
+ - lib/solid_observer/services/record_cache_metric.rb
116
137
  - lib/solid_observer/services/record_event.rb
138
+ - lib/solid_observer/services/storage_info_snapshot.rb
117
139
  - lib/solid_observer/services/ui_auth_check.rb
118
140
  - lib/solid_observer/subscriber.rb
119
141
  - lib/solid_observer/version.rb