solid_observer 0.1.1 → 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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +84 -0
  3. data/README.md +241 -59
  4. data/app/assets/javascripts/solid_observer/live_poll.js +376 -0
  5. data/app/assets/stylesheets/solid_observer/application.css +18 -0
  6. data/app/controllers/concerns/solid_observer/paginatable.rb +17 -0
  7. data/app/controllers/concerns/solid_observer/require_persistence_mode.rb +19 -0
  8. data/app/controllers/concerns/solid_observer/require_solid_queue.rb +19 -0
  9. data/app/controllers/solid_observer/application_controller.rb +69 -0
  10. data/app/controllers/solid_observer/cache_dashboard_controller.rb +59 -0
  11. data/app/controllers/solid_observer/cache_operations_controller.rb +24 -0
  12. data/app/controllers/solid_observer/dashboard_controller.rb +122 -0
  13. data/app/controllers/solid_observer/events_controller.rb +50 -0
  14. data/app/controllers/solid_observer/jobs_controller.rb +85 -0
  15. data/app/controllers/solid_observer/storages_controller.rb +12 -0
  16. data/app/helpers/solid_observer/application_helper.rb +244 -0
  17. data/app/helpers/solid_observer/dashboard_helper.rb +58 -0
  18. data/app/models/solid_observer/cache_event.rb +15 -0
  19. data/app/models/solid_observer/cache_metric.rb +14 -0
  20. data/app/models/solid_observer/queue_event.rb +134 -0
  21. data/app/models/solid_observer/queue_metric.rb +1 -1
  22. data/app/models/solid_observer/storage_info.rb +4 -1
  23. data/app/presenters/solid_observer/execution_presenter.rb +50 -0
  24. data/app/views/layouts/solid_observer/application.html.erb +597 -0
  25. data/app/views/solid_observer/cache_dashboard/_charts.html.erb +40 -0
  26. data/app/views/solid_observer/cache_dashboard/_recent_events.html.erb +34 -0
  27. data/app/views/solid_observer/cache_dashboard/_summary.html.erb +39 -0
  28. data/app/views/solid_observer/cache_dashboard/index.html.erb +62 -0
  29. data/app/views/solid_observer/cache_operations/_confirm_clear.html.erb +6 -0
  30. data/app/views/solid_observer/cache_operations/index.html.erb +60 -0
  31. data/app/views/solid_observer/dashboard/_chart.html.erb +28 -0
  32. data/app/views/solid_observer/dashboard/_live_state.html.erb +20 -0
  33. data/app/views/solid_observer/dashboard/_queue_table.html.erb +34 -0
  34. data/app/views/solid_observer/dashboard/_right_now.html.erb +3 -0
  35. data/app/views/solid_observer/dashboard/_throughput.html.erb +32 -0
  36. data/app/views/solid_observer/dashboard/index.html.erb +143 -0
  37. data/app/views/solid_observer/errors/storage_unavailable.html.erb +27 -0
  38. data/app/views/solid_observer/events/index.html.erb +53 -0
  39. data/app/views/solid_observer/events/show.html.erb +47 -0
  40. data/app/views/solid_observer/jobs/index.html.erb +61 -0
  41. data/app/views/solid_observer/jobs/show.html.erb +71 -0
  42. data/app/views/solid_observer/shared/_empty_state.html.erb +5 -0
  43. data/app/views/solid_observer/shared/_pagination.html.erb +17 -0
  44. data/app/views/solid_observer/shared/_stat_card.html.erb +9 -0
  45. data/app/views/solid_observer/storages/show.html.erb +71 -0
  46. data/bin/quality_gate +95 -0
  47. data/config/routes.rb +22 -0
  48. data/db/migrate/20260424000001_add_composite_indexes_to_queue_events.rb +30 -0
  49. data/db/migrate/20260601000001_create_solid_observer_cache_events.rb +22 -0
  50. data/db/migrate/20260601000002_create_solid_observer_cache_metrics.rb +18 -0
  51. data/db/migrate/20260602000001_add_component_to_solid_observer_storage_infos.rb +8 -0
  52. data/lib/generators/solid_observer/install_generator.rb +12 -25
  53. data/lib/generators/solid_observer/templates/initializer.rb.tt +6 -6
  54. data/lib/solid_observer/base_metric.rb +1 -1
  55. data/lib/solid_observer/cache_event_buffer.rb +53 -0
  56. data/lib/solid_observer/cache_subscriber.rb +47 -0
  57. data/lib/solid_observer/chart_buffer.rb +83 -0
  58. data/lib/solid_observer/cli/base.rb +2 -2
  59. data/lib/solid_observer/cli/jobs.rb +2 -2
  60. data/lib/solid_observer/cli/status.rb +20 -2
  61. data/lib/solid_observer/cli/storage.rb +48 -44
  62. data/lib/solid_observer/configuration.rb +67 -38
  63. data/lib/solid_observer/correlation_id_resolver.rb +8 -6
  64. data/lib/solid_observer/engine.rb +110 -18
  65. data/lib/solid_observer/params/events_filter.rb +37 -0
  66. data/lib/solid_observer/params/jobs_filter.rb +35 -0
  67. data/lib/solid_observer/queries/events_query.rb +27 -0
  68. data/lib/solid_observer/queries/execution_finder.rb +42 -0
  69. data/lib/solid_observer/queries/job_executions_query.rb +73 -0
  70. data/lib/solid_observer/queue_event_buffer.rb +163 -25
  71. data/lib/solid_observer/queue_stats.rb +165 -19
  72. data/lib/solid_observer/services/cache_operations.rb +115 -0
  73. data/lib/solid_observer/services/cache_stats.rb +329 -0
  74. data/lib/solid_observer/services/cleanup_storage.rb +73 -41
  75. data/lib/solid_observer/services/database_size.rb +91 -0
  76. data/lib/solid_observer/services/flush_cache_event_buffer.rb +54 -0
  77. data/lib/solid_observer/services/flush_event_buffer.rb +31 -15
  78. data/lib/solid_observer/services/install_migrations.rb +49 -0
  79. data/lib/solid_observer/services/record_cache_event.rb +142 -0
  80. data/lib/solid_observer/services/record_cache_metric.rb +74 -0
  81. data/lib/solid_observer/services/record_event.rb +51 -14
  82. data/lib/solid_observer/services/storage_info_snapshot.rb +128 -0
  83. data/lib/solid_observer/services/ui_auth_check.rb +65 -0
  84. data/lib/solid_observer/subscriber.rb +15 -8
  85. data/lib/solid_observer/version.rb +1 -1
  86. data/lib/solid_observer.rb +7 -0
  87. data/lib/tasks/solid_observer.rake +39 -2
  88. metadata +77 -1
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidObserver
4
+ module Services
5
+ class InstallMigrations
6
+ def self.call(rails_env: Rails.env)
7
+ new(rails_env).call
8
+ end
9
+
10
+ def initialize(rails_env)
11
+ @rails_env = rails_env
12
+ end
13
+
14
+ def call
15
+ destination = resolve_destination
16
+ copied = ActiveRecord::Migration.copy(
17
+ destination,
18
+ "solid_observer" => SolidObserver::Engine.paths["db/migrate"].existent.first
19
+ )
20
+
21
+ {
22
+ destination: destination,
23
+ copied: copied
24
+ }
25
+ end
26
+
27
+ private
28
+
29
+ def resolve_destination
30
+ path = configured_path
31
+ return path if path
32
+
33
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths.first || "db/migrate"
34
+ end
35
+
36
+ def configured_path
37
+ config = ActiveRecord::Base.configurations.configs_for(
38
+ env_name: @rails_env,
39
+ name: "solid_observer_queue"
40
+ )
41
+ path = Array(config&.migrations_paths).first
42
+ return if path.blank?
43
+
44
+ FileUtils.mkdir_p(path) unless Dir.exist?(path)
45
+ path
46
+ end
47
+ end
48
+ end
49
+ end
@@ -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
@@ -54,32 +54,69 @@ module SolidObserver
54
54
  job_class: metadata[:job_class],
55
55
  queue_name: metadata[:queue_name],
56
56
  correlation_id: CorrelationIdResolver.resolve(@event),
57
- duration: @event.duration,
57
+ duration: duration_in_seconds,
58
58
  metadata: metadata.except(:job_class, :queue_name).to_json,
59
59
  recorded_at: Time.current
60
60
  }
61
61
  end
62
62
 
63
+ def duration_in_seconds
64
+ @event.duration&./(1000.0)
65
+ end
66
+
63
67
  def extract_metadata
64
68
  payload = @event.payload || {}
65
- exception_obj = payload[:exception_object]
66
-
67
- {
68
- job_id: payload.dig(:job, :job_id),
69
- job_class: payload.dig(:job, :class_name) || payload.dig(:job, :job_class),
70
- queue_name: payload.dig(:job, :queue_name),
71
- arguments: payload.dig(:job, :arguments),
72
- executions: payload.dig(:job, :executions),
73
- exception_class: exception_obj&.class&.name || payload[:exception]&.first,
74
- exception_message: exception_obj&.message || payload[:exception]&.last,
75
- enqueued_at: payload.dig(:job, :enqueued_at),
76
- priority: payload.dig(:job, :priority)
77
- }.compact
69
+ metadata_from(payload, payload[:job]).compact
78
70
  rescue => e
79
71
  Rails.logger.warn "[SolidObserver] Failed to extract metadata: #{e.message}" if defined?(Rails)
80
72
  {}
81
73
  end
82
74
 
75
+ def metadata_from(payload, job)
76
+ {
77
+ job_id: read_job_attr(job, :job_id),
78
+ job_class: read_job_class(job),
79
+ queue_name: read_job_attr(job, :queue_name),
80
+ executions: read_job_attr(job, :executions),
81
+ exception_class: exception_class(payload),
82
+ exception_message: exception_message(payload),
83
+ enqueued_at: read_job_attr(job, :enqueued_at),
84
+ priority: read_job_attr(job, :priority)
85
+ }
86
+ end
87
+
88
+ def read_job_attr(job, attr)
89
+ return nil unless job
90
+
91
+ if job.is_a?(Hash)
92
+ job[attr]
93
+ else
94
+ job.public_send(attr)
95
+ end
96
+ rescue NoMethodError
97
+ nil
98
+ end
99
+
100
+ def read_job_class(job)
101
+ return nil unless job
102
+
103
+ if job.is_a?(Hash)
104
+ job[:class_name] || job[:job_class]
105
+ else
106
+ job.class.name
107
+ end
108
+ end
109
+
110
+ def exception_class(payload)
111
+ exception_obj = payload[:exception_object]
112
+ exception_obj&.class&.name || payload[:exception]&.first
113
+ end
114
+
115
+ def exception_message(payload)
116
+ exception_obj = payload[:exception_object]
117
+ exception_obj&.message || payload[:exception]&.last
118
+ end
119
+
83
120
  def increment_metric
84
121
  return unless SolidObserver.config.persistence_mode?
85
122
 
@@ -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
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidObserver
4
+ module Services
5
+ # Logs a boot-time WARNING when the Web UI's HTTP Basic Auth is misconfigured.
6
+ #
7
+ # No-ops when the UI is disabled or both credentials are set. Otherwise logs
8
+ # one of two warnings: "no auth configured" (neither credential set) or
9
+ # "auth misconfigured" (exactly one set, naming the missing one).
10
+ #
11
+ # The UI ships fail-open on partial credentials — see
12
+ # SolidObserver::ApplicationController#authenticate.
13
+ class UiAuthCheck
14
+ def self.call(config:, logger: Rails.logger)
15
+ new(config, logger).call
16
+ end
17
+
18
+ def initialize(config, logger)
19
+ @config = config
20
+ @logger = logger
21
+ end
22
+
23
+ def call
24
+ return unless config.ui_enabled
25
+
26
+ warning = warning_message
27
+ logger.warn(warning) if warning
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :config, :logger
33
+
34
+ def warning_message
35
+ return nil if both_credentials_present?
36
+ return no_auth_warning if neither_credential_present?
37
+
38
+ partial_auth_warning
39
+ end
40
+
41
+ def both_credentials_present?
42
+ config.ui_username.present? && config.ui_password.present?
43
+ end
44
+
45
+ def neither_credential_present?
46
+ config.ui_username.blank? && config.ui_password.blank?
47
+ end
48
+
49
+ def no_auth_warning
50
+ "[SolidObserver] WARNING: UI is enabled with no authentication configured. " \
51
+ "Set config.ui_username and config.ui_password."
52
+ end
53
+
54
+ def partial_auth_warning
55
+ set, missing = partial_credential_names
56
+ "[SolidObserver] WARNING: UI authentication is misconfigured — #{set} is set but #{missing} is missing/nil. " \
57
+ "The UI will ship UNAUTHENTICATED until both are configured. Set both credentials, or unset both to silence this warning."
58
+ end
59
+
60
+ def partial_credential_names
61
+ config.ui_username.present? ? %w[ui_username ui_password] : %w[ui_password ui_username]
62
+ end
63
+ end
64
+ end
65
+ end
@@ -18,15 +18,9 @@ module SolidObserver
18
18
 
19
19
  class << self
20
20
  def subscribe!
21
- return unless SolidObserver.config.observe_queue
22
- return if subscribed?
21
+ return unless subscription_allowed?
23
22
 
24
- @subscriptions = []
25
- @subscriptions << subscribe_to_enqueue
26
- @subscriptions << subscribe_to_perform
27
- @subscriptions << subscribe_to_retry_stopped
28
- @subscriptions << subscribe_to_discard
29
- @subscriptions.compact!
23
+ @subscriptions = subscriptions_for_events.compact
30
24
  end
31
25
 
32
26
  def unsubscribe!
@@ -44,6 +38,19 @@ module SolidObserver
44
38
 
45
39
  private
46
40
 
41
+ def subscription_allowed?
42
+ SolidObserver.config.observe_queue && !subscribed?
43
+ end
44
+
45
+ def subscriptions_for_events
46
+ [
47
+ subscribe_to_enqueue,
48
+ subscribe_to_perform,
49
+ subscribe_to_retry_stopped,
50
+ subscribe_to_discard
51
+ ]
52
+ end
53
+
47
54
  def subscribe_to_enqueue
48
55
  ActiveSupport::Notifications.subscribe("enqueue.active_job") do |*args|
49
56
  event = ActiveSupport::Notifications::Event.new(*args)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidObserver
4
- VERSION = "0.1.1"
4
+ VERSION = "0.4.0"
5
5
  RUBY_MINIMUM_VERSION = "3.2.0"
6
6
  RAILS_MINIMUM_VERSION = "8.0"
7
7
  end
@@ -5,9 +5,15 @@ require_relative "solid_observer/configuration"
5
5
  require_relative "solid_observer/correlation_id_resolver"
6
6
  require_relative "solid_observer/base_event" if defined?(ActiveRecord)
7
7
  require_relative "solid_observer/base_metric" if defined?(ActiveRecord)
8
+ require_relative "solid_observer/params/jobs_filter"
9
+ require_relative "solid_observer/params/events_filter"
10
+ require_relative "solid_observer/queries/job_executions_query" if defined?(ActiveRecord)
11
+ require_relative "solid_observer/queries/events_query" if defined?(ActiveRecord)
12
+ require_relative "solid_observer/queries/execution_finder" if defined?(ActiveRecord)
8
13
  require_relative "solid_observer/services/record_event" if defined?(ActiveRecord)
9
14
  require_relative "solid_observer/services/flush_event_buffer" if defined?(ActiveRecord)
10
15
  require_relative "solid_observer/services/cleanup_storage" if defined?(ActiveRecord)
16
+ require_relative "solid_observer/services/ui_auth_check"
11
17
  require_relative "solid_observer/queue_event_buffer" if defined?(ActiveRecord)
12
18
  require_relative "solid_observer/subscriber" if defined?(ActiveSupport)
13
19
  require_relative "solid_observer/cli/base"
@@ -15,6 +21,7 @@ require_relative "solid_observer/cli/status"
15
21
  require_relative "solid_observer/cli/storage"
16
22
  require_relative "solid_observer/cli/jobs"
17
23
  require_relative "solid_observer/queue_stats"
24
+ require_relative "../app/presenters/solid_observer/execution_presenter"
18
25
  require_relative "solid_observer/engine" if defined?(Rails::Engine)
19
26
 
20
27
  module SolidObserver
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../solid_observer/services/install_migrations"
4
+
3
5
  namespace :solid_observer do
4
6
  desc "Display SolidObserver version"
5
7
  task :version do
@@ -11,8 +13,14 @@ namespace :solid_observer do
11
13
  namespace :install do
12
14
  desc "Copy SolidObserver migrations to your application"
13
15
  task migrations: :environment do
14
- Rake::Task["railties:install:migrations"].reenable
15
- Rake::Task["railties:install:migrations"].invoke
16
+ result = SolidObserver::Services::InstallMigrations.call
17
+
18
+ if result[:copied].any?
19
+ suffix = (result[:copied].size == 1) ? "" : "s"
20
+ puts "Copied #{result[:copied].size} SolidObserver migration#{suffix} to #{result[:destination]}/"
21
+ else
22
+ puts "No new SolidObserver migrations to copy (all already present in #{result[:destination]}/)"
23
+ end
16
24
  end
17
25
  end
18
26
 
@@ -60,6 +68,35 @@ namespace :solid_observer do
60
68
  end
61
69
  end
62
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
+
63
100
  namespace :storage do
64
101
  desc "Run storage cleanup based on retention policy"
65
102
  task cleanup: :environment do