solid_observer 0.1.0 → 0.3.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 +73 -0
- data/README.md +198 -36
- data/app/assets/javascripts/solid_observer/live_poll.js +376 -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/dashboard_controller.rb +79 -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 +95 -0
- data/app/helpers/solid_observer/dashboard_helper.rb +39 -0
- data/app/models/solid_observer/queue_event.rb +134 -0
- data/app/models/solid_observer/queue_metric.rb +1 -1
- data/app/presenters/solid_observer/execution_presenter.rb +50 -0
- data/app/views/layouts/solid_observer/application.html.erb +470 -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 +113 -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 +39 -0
- data/bin/quality_gate +95 -0
- data/config/routes.rb +17 -0
- data/db/migrate/20260424000001_add_composite_indexes_to_queue_events.rb +30 -0
- data/lib/generators/solid_observer/install_generator.rb +12 -25
- data/lib/generators/solid_observer/templates/initializer.rb.tt +5 -6
- data/lib/solid_observer/base_metric.rb +1 -1
- 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 +41 -32
- data/lib/solid_observer/configuration.rb +67 -34
- data/lib/solid_observer/correlation_id_resolver.rb +8 -6
- data/lib/solid_observer/engine.rb +75 -15
- 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 -22
- data/lib/solid_observer/queue_stats.rb +165 -19
- data/lib/solid_observer/services/cleanup_storage.rb +60 -42
- data/lib/solid_observer/services/database_size.rb +86 -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_event.rb +53 -14
- 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 +10 -2
- metadata +55 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "database_size"
|
|
4
|
+
|
|
3
5
|
module SolidObserver
|
|
4
6
|
module Services
|
|
5
7
|
class CleanupStorage
|
|
@@ -8,86 +10,102 @@ module SolidObserver
|
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
def call
|
|
11
|
-
|
|
13
|
+
return 0 if SolidObserver.config.realtime_mode?
|
|
14
|
+
|
|
15
|
+
deleted_count = perform_cleanup_transaction
|
|
16
|
+
post_cleanup(deleted_count)
|
|
17
|
+
rescue => e
|
|
18
|
+
handle_cleanup_failure(e)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def handle_cleanup_failure(error)
|
|
24
|
+
Rails.logger.error "[SolidObserver] Cleanup failed: #{error.message}"
|
|
25
|
+
raise
|
|
26
|
+
end
|
|
12
27
|
|
|
28
|
+
def perform_cleanup_transaction
|
|
13
29
|
QueueEvent.transaction do
|
|
14
30
|
deleted_count = delete_old_events
|
|
15
31
|
record_snapshot_after_cleanup
|
|
32
|
+
deleted_count
|
|
16
33
|
end
|
|
34
|
+
end
|
|
17
35
|
|
|
36
|
+
def post_cleanup(deleted_count)
|
|
18
37
|
vacuum_database
|
|
19
|
-
|
|
20
38
|
check_storage_warnings
|
|
21
39
|
log_results(deleted_count)
|
|
22
|
-
|
|
23
40
|
deleted_count
|
|
24
|
-
rescue => e
|
|
25
|
-
Rails.logger.error "[SolidObserver] Cleanup failed: #{e.message}"
|
|
26
|
-
raise
|
|
27
41
|
end
|
|
28
42
|
|
|
29
|
-
private
|
|
30
|
-
|
|
31
43
|
def delete_old_events
|
|
32
44
|
cutoff = SolidObserver.config.event_retention.ago
|
|
33
45
|
QueueEvent.where("recorded_at < ?", cutoff).delete_all
|
|
34
46
|
end
|
|
35
47
|
|
|
36
48
|
def record_snapshot_after_cleanup
|
|
37
|
-
|
|
38
|
-
event_count = QueueEvent.count
|
|
39
|
-
|
|
49
|
+
# StorageInfo.db_size_bytes is NOT NULL; record_snapshot coerces nil to 0.
|
|
40
50
|
StorageInfo.record_snapshot(
|
|
41
|
-
db_size:
|
|
42
|
-
event_count:
|
|
51
|
+
db_size: current_database_size,
|
|
52
|
+
event_count: QueueEvent.count
|
|
43
53
|
)
|
|
44
54
|
end
|
|
45
55
|
|
|
46
56
|
def vacuum_database
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
when "postgresql"
|
|
52
|
-
QueueEvent.connection.execute("VACUUM ANALYZE solid_observer_queue_events")
|
|
53
|
-
when "mysql2", "trilogy"
|
|
54
|
-
QueueEvent.connection.execute("OPTIMIZE TABLE solid_observer_queue_events")
|
|
55
|
-
end
|
|
57
|
+
statement = maintenance_statement
|
|
58
|
+
return unless statement
|
|
59
|
+
|
|
60
|
+
QueueEvent.connection.execute(statement)
|
|
56
61
|
rescue => e
|
|
57
62
|
Rails.logger.warn "[SolidObserver] Database maintenance failed: #{e.message}"
|
|
58
63
|
end
|
|
59
64
|
|
|
60
|
-
def
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
Rails.logger.warn
|
|
65
|
-
0
|
|
65
|
+
def check_storage_warnings
|
|
66
|
+
current_size = current_database_size
|
|
67
|
+
return unless warning_needed?(current_size)
|
|
68
|
+
|
|
69
|
+
Rails.logger.warn(storage_warning_message(current_size))
|
|
66
70
|
end
|
|
67
71
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
threshold = SolidObserver.config.warning_threshold
|
|
71
|
-
current_size = calculate_database_size
|
|
72
|
+
def warning_needed?(current_size)
|
|
73
|
+
return false unless current_size
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
config = SolidObserver.config
|
|
76
|
+
max_size = config.max_db_size
|
|
77
|
+
threshold = config.warning_threshold
|
|
78
|
+
current_size > (max_size * threshold)
|
|
79
|
+
end
|
|
74
80
|
|
|
81
|
+
def storage_warning_message(current_size)
|
|
82
|
+
max_size = SolidObserver.config.max_db_size
|
|
75
83
|
percentage = ((current_size.to_f / max_size) * 100).round(1)
|
|
76
|
-
|
|
84
|
+
current_size_human = human_size(current_size)
|
|
85
|
+
max_size_human = human_size(max_size)
|
|
86
|
+
"[SolidObserver] Queue DB approaching limit: #{current_size_human} / #{max_size_human} (#{percentage}%)"
|
|
77
87
|
end
|
|
78
88
|
|
|
79
|
-
def
|
|
80
|
-
|
|
89
|
+
def human_size(bytes)
|
|
90
|
+
ActiveSupport::NumberHelper.number_to_human_size(bytes, precision: 1, significant: false, strip_insignificant_zeros: false)
|
|
81
91
|
end
|
|
82
92
|
|
|
83
|
-
def
|
|
84
|
-
return
|
|
93
|
+
def current_database_size
|
|
94
|
+
return @current_database_size if defined?(@current_database_size)
|
|
95
|
+
|
|
96
|
+
@current_database_size = DatabaseSize.call(connection: QueueEvent.connection)
|
|
97
|
+
end
|
|
85
98
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
99
|
+
def log_results(deleted_count)
|
|
100
|
+
Rails.logger.info "[SolidObserver] Cleaned #{deleted_count} queue events"
|
|
101
|
+
end
|
|
89
102
|
|
|
90
|
-
|
|
103
|
+
def maintenance_statement
|
|
104
|
+
case QueueEvent.connection.adapter_name.downcase
|
|
105
|
+
when "sqlite" then "VACUUM"
|
|
106
|
+
when "postgresql" then "VACUUM ANALYZE solid_observer_queue_events"
|
|
107
|
+
when "mysql2", "trilogy" then "OPTIMIZE TABLE solid_observer_queue_events"
|
|
108
|
+
end
|
|
91
109
|
end
|
|
92
110
|
end
|
|
93
111
|
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
module Services
|
|
5
|
+
# Returns bytes used by solid_observer_queue_events across supported adapters.
|
|
6
|
+
#
|
|
7
|
+
# SQLite uses whole-database page accounting; PostgreSQL and MySQL/Trilogy
|
|
8
|
+
# use table + index size from adapter-native system functions.
|
|
9
|
+
class DatabaseSize
|
|
10
|
+
TABLE_NAME = "solid_observer_queue_events"
|
|
11
|
+
|
|
12
|
+
def self.call(connection:)
|
|
13
|
+
new(connection).call
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(connection)
|
|
17
|
+
@connection = connection
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call
|
|
21
|
+
fetch_size
|
|
22
|
+
rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished => e
|
|
23
|
+
log_query_failure(e.message)
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :connection
|
|
30
|
+
|
|
31
|
+
def adapter_key
|
|
32
|
+
case connection.adapter_name.to_s.downcase
|
|
33
|
+
when /sqlite/ then :sqlite
|
|
34
|
+
when /postgres|postgis/ then :postgresql
|
|
35
|
+
when "mysql2", "trilogy" then :mysql
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def fetch_size
|
|
40
|
+
case adapter_key
|
|
41
|
+
when :sqlite then sqlite_size
|
|
42
|
+
when :postgresql then postgresql_size
|
|
43
|
+
when :mysql then mysql_size
|
|
44
|
+
else
|
|
45
|
+
unknown_adapter_size
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def unknown_adapter_size
|
|
50
|
+
log_unknown_adapter
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def sqlite_size
|
|
55
|
+
connection.query_value("SELECT pragma_page_count() * pragma_page_size()")&.to_i
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def postgresql_size
|
|
59
|
+
quoted_table = connection.quote(TABLE_NAME)
|
|
60
|
+
connection.query_value("SELECT pg_total_relation_size(#{quoted_table})")&.to_i
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def mysql_size
|
|
64
|
+
quoted_table = connection.quote(TABLE_NAME)
|
|
65
|
+
|
|
66
|
+
connection.query_value(<<~SQL)&.to_i
|
|
67
|
+
SELECT COALESCE(data_length + index_length, 0)
|
|
68
|
+
FROM information_schema.tables
|
|
69
|
+
WHERE table_schema = DATABASE()
|
|
70
|
+
AND table_name = #{quoted_table}
|
|
71
|
+
SQL
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def log_unknown_adapter
|
|
75
|
+
Rails.logger&.warn(
|
|
76
|
+
"[SolidObserver] Unknown adapter for DatabaseSize: " \
|
|
77
|
+
"#{connection.adapter_name.inspect} — storage monitoring disabled"
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def log_query_failure(message)
|
|
82
|
+
Rails.logger&.warn("[SolidObserver] DatabaseSize query failed: #{message}")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -26,31 +26,47 @@ module SolidObserver
|
|
|
26
26
|
def call
|
|
27
27
|
return 0 if @events.empty?
|
|
28
28
|
|
|
29
|
+
bulk_insert
|
|
30
|
+
@events.size
|
|
31
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::StatementInvalid => e
|
|
32
|
+
handle_bulk_insert_failure(e)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def bulk_insert
|
|
29
38
|
QueueEvent.transaction do
|
|
30
39
|
QueueEvent.insert_all!(@events)
|
|
31
40
|
end
|
|
41
|
+
end
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
log_error("Bulk insert failed, retrying in batches: #{e.message}")
|
|
43
|
+
def handle_bulk_insert_failure(error)
|
|
44
|
+
log_error("Bulk insert failed, retrying in batches: #{error.message}")
|
|
36
45
|
retry_with_smaller_batches
|
|
37
46
|
end
|
|
38
47
|
|
|
39
|
-
private
|
|
40
|
-
|
|
41
48
|
def retry_with_smaller_batches
|
|
42
|
-
inserted =
|
|
49
|
+
inserted = @events.each_slice(BATCH_SIZE).sum { |batch| insert_batch(batch) }
|
|
50
|
+
log_failed_count if @failed_count.positive?
|
|
51
|
+
inserted
|
|
52
|
+
end
|
|
43
53
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
def insert_batch(batch)
|
|
55
|
+
QueueEvent.insert_all(batch, returning: false)
|
|
56
|
+
batch.size
|
|
57
|
+
rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordInvalid => e
|
|
58
|
+
register_failed_batch(batch, e)
|
|
59
|
+
0
|
|
60
|
+
end
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
def register_failed_batch(batch, error)
|
|
63
|
+
batch_size = batch.size
|
|
64
|
+
@failed_count += batch_size
|
|
65
|
+
log_warning("Failed to insert batch of #{batch_size} events: #{error.message}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def log_failed_count
|
|
69
|
+
log_warning("#{@failed_count} events could not be saved")
|
|
54
70
|
end
|
|
55
71
|
|
|
56
72
|
def log_error(message)
|
|
@@ -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
|
|
@@ -54,33 +54,72 @@ 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
|
|
121
|
+
return unless SolidObserver.config.persistence_mode?
|
|
122
|
+
|
|
84
123
|
period = Time.current.beginning_of_hour
|
|
85
124
|
QueueMetric.increment(metric: @metric_name, period: period)
|
|
86
125
|
rescue => e
|
|
@@ -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
|
|