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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +84 -0
- data/README.md +241 -59
- data/app/assets/javascripts/solid_observer/live_poll.js +376 -0
- data/app/assets/stylesheets/solid_observer/application.css +18 -0
- data/app/controllers/concerns/solid_observer/paginatable.rb +17 -0
- data/app/controllers/concerns/solid_observer/require_persistence_mode.rb +19 -0
- data/app/controllers/concerns/solid_observer/require_solid_queue.rb +19 -0
- data/app/controllers/solid_observer/application_controller.rb +69 -0
- data/app/controllers/solid_observer/cache_dashboard_controller.rb +59 -0
- data/app/controllers/solid_observer/cache_operations_controller.rb +24 -0
- data/app/controllers/solid_observer/dashboard_controller.rb +122 -0
- data/app/controllers/solid_observer/events_controller.rb +50 -0
- data/app/controllers/solid_observer/jobs_controller.rb +85 -0
- data/app/controllers/solid_observer/storages_controller.rb +12 -0
- data/app/helpers/solid_observer/application_helper.rb +244 -0
- data/app/helpers/solid_observer/dashboard_helper.rb +58 -0
- data/app/models/solid_observer/cache_event.rb +15 -0
- data/app/models/solid_observer/cache_metric.rb +14 -0
- data/app/models/solid_observer/queue_event.rb +134 -0
- data/app/models/solid_observer/queue_metric.rb +1 -1
- data/app/models/solid_observer/storage_info.rb +4 -1
- data/app/presenters/solid_observer/execution_presenter.rb +50 -0
- data/app/views/layouts/solid_observer/application.html.erb +597 -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/_chart.html.erb +28 -0
- data/app/views/solid_observer/dashboard/_live_state.html.erb +20 -0
- data/app/views/solid_observer/dashboard/_queue_table.html.erb +34 -0
- data/app/views/solid_observer/dashboard/_right_now.html.erb +3 -0
- data/app/views/solid_observer/dashboard/_throughput.html.erb +32 -0
- data/app/views/solid_observer/dashboard/index.html.erb +143 -0
- data/app/views/solid_observer/errors/storage_unavailable.html.erb +27 -0
- data/app/views/solid_observer/events/index.html.erb +53 -0
- data/app/views/solid_observer/events/show.html.erb +47 -0
- data/app/views/solid_observer/jobs/index.html.erb +61 -0
- data/app/views/solid_observer/jobs/show.html.erb +71 -0
- data/app/views/solid_observer/shared/_empty_state.html.erb +5 -0
- data/app/views/solid_observer/shared/_pagination.html.erb +17 -0
- data/app/views/solid_observer/shared/_stat_card.html.erb +9 -0
- data/app/views/solid_observer/storages/show.html.erb +71 -0
- data/bin/quality_gate +95 -0
- data/config/routes.rb +22 -0
- data/db/migrate/20260424000001_add_composite_indexes_to_queue_events.rb +30 -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/lib/generators/solid_observer/install_generator.rb +12 -25
- data/lib/generators/solid_observer/templates/initializer.rb.tt +6 -6
- data/lib/solid_observer/base_metric.rb +1 -1
- data/lib/solid_observer/cache_event_buffer.rb +53 -0
- data/lib/solid_observer/cache_subscriber.rb +47 -0
- data/lib/solid_observer/chart_buffer.rb +83 -0
- data/lib/solid_observer/cli/base.rb +2 -2
- data/lib/solid_observer/cli/jobs.rb +2 -2
- data/lib/solid_observer/cli/status.rb +20 -2
- data/lib/solid_observer/cli/storage.rb +48 -44
- data/lib/solid_observer/configuration.rb +67 -38
- data/lib/solid_observer/correlation_id_resolver.rb +8 -6
- data/lib/solid_observer/engine.rb +110 -18
- data/lib/solid_observer/params/events_filter.rb +37 -0
- data/lib/solid_observer/params/jobs_filter.rb +35 -0
- data/lib/solid_observer/queries/events_query.rb +27 -0
- data/lib/solid_observer/queries/execution_finder.rb +42 -0
- data/lib/solid_observer/queries/job_executions_query.rb +73 -0
- data/lib/solid_observer/queue_event_buffer.rb +163 -25
- data/lib/solid_observer/queue_stats.rb +165 -19
- data/lib/solid_observer/services/cache_operations.rb +115 -0
- data/lib/solid_observer/services/cache_stats.rb +329 -0
- data/lib/solid_observer/services/cleanup_storage.rb +73 -41
- data/lib/solid_observer/services/database_size.rb +91 -0
- data/lib/solid_observer/services/flush_cache_event_buffer.rb +54 -0
- data/lib/solid_observer/services/flush_event_buffer.rb +31 -15
- data/lib/solid_observer/services/install_migrations.rb +49 -0
- data/lib/solid_observer/services/record_cache_event.rb +142 -0
- data/lib/solid_observer/services/record_cache_metric.rb +74 -0
- data/lib/solid_observer/services/record_event.rb +51 -14
- data/lib/solid_observer/services/storage_info_snapshot.rb +128 -0
- data/lib/solid_observer/services/ui_auth_check.rb +65 -0
- data/lib/solid_observer/subscriber.rb +15 -8
- data/lib/solid_observer/version.rb +1 -1
- data/lib/solid_observer.rb +7 -0
- data/lib/tasks/solid_observer.rake +39 -2
- 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:
|
|
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
|
-
|
|
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
|
|
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)
|
data/lib/solid_observer.rb
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
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
|