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
|
@@ -9,10 +9,9 @@ SolidObserver.configure do |config|
|
|
|
9
9
|
# Recommended: false in production, true in development/staging
|
|
10
10
|
config.ui_enabled = !Rails.env.production?
|
|
11
11
|
|
|
12
|
-
# Authentication for UI
|
|
13
|
-
# config.
|
|
14
|
-
# config.
|
|
15
|
-
# config.http_basic_auth_password = "secret"
|
|
12
|
+
# Authentication for web UI — set a username to enable HTTP Basic Auth
|
|
13
|
+
# config.ui_username = "admin"
|
|
14
|
+
# config.ui_password = "secret"
|
|
16
15
|
|
|
17
16
|
# Base controller for UI (customize authorization)
|
|
18
17
|
# config.ui_base_controller = "ApplicationController"
|
|
@@ -20,11 +19,11 @@ SolidObserver.configure do |config|
|
|
|
20
19
|
# === Queue Observability (v0.1.0) ===
|
|
21
20
|
config.observe_queue = true
|
|
22
21
|
|
|
23
|
-
# === Cache Observability (Coming in v0.
|
|
22
|
+
# === Cache Observability (Coming in v0.4.0+) ===
|
|
24
23
|
# config.observe_cache = true
|
|
25
24
|
# config.cache_sampling_rate = 0.1 # Sample 10% of cache operations
|
|
26
25
|
|
|
27
|
-
# === Cable Observability (Coming in v0.
|
|
26
|
+
# === Cable Observability (Coming in v0.5.0+) ===
|
|
28
27
|
# config.observe_cable = true
|
|
29
28
|
|
|
30
29
|
# Data Retention
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module SolidObserver
|
|
4
4
|
# BaseMetric provides the foundation for time-series metrics storage.
|
|
5
5
|
#
|
|
6
|
-
# NOTE: Metrics functionality is planned for
|
|
6
|
+
# NOTE: Metrics functionality is planned for a future release. The database connection
|
|
7
7
|
# will be configured by the Engine (similar to BaseEvent) when metrics are
|
|
8
8
|
# fully implemented.
|
|
9
9
|
#
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
class ChartBuffer
|
|
5
|
+
INSTANCE_MUTEX = Mutex.new
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def append(value, at: Time.now)
|
|
9
|
+
instance.append(value, at: at)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def recent(window_seconds)
|
|
13
|
+
instance.recent(window_seconds)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def clear
|
|
17
|
+
instance.clear
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def instance
|
|
23
|
+
INSTANCE_MUTEX.synchronize { @instance ||= new }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize
|
|
28
|
+
@mutex = Mutex.new
|
|
29
|
+
@samples = []
|
|
30
|
+
@cap = nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def append(value, at: Time.now)
|
|
34
|
+
sample = {t: at.to_i, v: value.to_i}
|
|
35
|
+
|
|
36
|
+
@mutex.synchronize { store_sample(sample) }
|
|
37
|
+
|
|
38
|
+
sample
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def recent(window_seconds)
|
|
42
|
+
cutoff = Time.now.to_i - window_seconds.to_i
|
|
43
|
+
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
@samples.select { |sample| sample[:t] >= cutoff }.map(&:dup)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def clear
|
|
50
|
+
@mutex.synchronize do
|
|
51
|
+
@samples.clear
|
|
52
|
+
@cap = nil
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def store_sample(sample)
|
|
59
|
+
@cap ||= compute_cap
|
|
60
|
+
replace_or_append(sample)
|
|
61
|
+
trim_to_cap
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def replace_or_append(sample)
|
|
65
|
+
latest_sample = @samples.last
|
|
66
|
+
|
|
67
|
+
if latest_sample && latest_sample[:t] == sample[:t]
|
|
68
|
+
@samples[-1] = sample
|
|
69
|
+
else
|
|
70
|
+
@samples << sample
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def trim_to_cap
|
|
75
|
+
overflow = @samples.length - @cap
|
|
76
|
+
@samples.shift(overflow) if overflow.positive?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def compute_cap
|
|
80
|
+
(3600 / 5).to_i # 720 samples — 1h at the 5s cadence
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -39,8 +39,8 @@ module SolidObserver
|
|
|
39
39
|
|
|
40
40
|
widths = calculate_column_widths(headers, rows)
|
|
41
41
|
|
|
42
|
-
output(format_table_row(headers, widths), color: :
|
|
43
|
-
output(separator_line(widths), color: :
|
|
42
|
+
output(format_table_row(headers, widths), color: :red)
|
|
43
|
+
output(separator_line(widths), color: :red)
|
|
44
44
|
|
|
45
45
|
rows.each do |row|
|
|
46
46
|
output(format_table_row(row, widths))
|
|
@@ -3,16 +3,34 @@
|
|
|
3
3
|
module SolidObserver
|
|
4
4
|
module CLI
|
|
5
5
|
class Status < Base
|
|
6
|
+
BANNER_ICON_TOP = " ┌─ ─┐"
|
|
7
|
+
BANNER_ICON_MID_LEFT = " ◉"
|
|
8
|
+
BANNER_ICON_BOT = " └─ ─┘"
|
|
9
|
+
BANNER_NAME = "solid_observer"
|
|
10
|
+
BANNER_NAME_GAP = " "
|
|
11
|
+
|
|
6
12
|
def call
|
|
13
|
+
print_banner
|
|
7
14
|
print_header
|
|
8
15
|
print_queue_stats
|
|
9
16
|
end
|
|
10
17
|
|
|
11
18
|
private
|
|
12
19
|
|
|
20
|
+
def print_banner
|
|
21
|
+
output(BANNER_ICON_TOP, color: :red)
|
|
22
|
+
output(banner_middle_line)
|
|
23
|
+
output(BANNER_ICON_BOT, color: :red)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def banner_middle_line
|
|
27
|
+
icon = color_enabled? ? colorize(BANNER_ICON_MID_LEFT, :red) : BANNER_ICON_MID_LEFT
|
|
28
|
+
"#{icon}#{BANNER_NAME_GAP}#{BANNER_NAME}"
|
|
29
|
+
end
|
|
30
|
+
|
|
13
31
|
def print_header
|
|
14
|
-
output("\n📊 SolidObserver Status", color: :
|
|
15
|
-
output("=" * 50, color: :
|
|
32
|
+
output("\n📊 SolidObserver Status", color: :red)
|
|
33
|
+
output("=" * 50, color: :red)
|
|
16
34
|
output("")
|
|
17
35
|
end
|
|
18
36
|
|
|
@@ -5,23 +5,31 @@ module SolidObserver
|
|
|
5
5
|
class Storage < Base
|
|
6
6
|
def call
|
|
7
7
|
print_section_header("💾 Storage Status")
|
|
8
|
+
return print_realtime_mode_message if SolidObserver.config.realtime_mode?
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
render_storage_status
|
|
11
|
+
end
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def print_realtime_mode_message
|
|
16
|
+
info("Storage monitoring is not available in real-time mode.")
|
|
17
|
+
info("Switch to persistence mode for event history and storage tracking.")
|
|
18
|
+
output("")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def render_storage_status
|
|
22
|
+
current_stats = gather_storage_stats
|
|
23
|
+
error_message = current_stats[:error]
|
|
24
|
+
return error(error_message) if error_message
|
|
15
25
|
|
|
16
26
|
print_storage_table(current_stats)
|
|
17
27
|
print_configuration
|
|
18
28
|
end
|
|
19
29
|
|
|
20
|
-
private
|
|
21
|
-
|
|
22
30
|
def gather_storage_stats
|
|
23
31
|
{
|
|
24
|
-
db_size_bytes:
|
|
32
|
+
db_size_bytes: SolidObserver::Services::DatabaseSize.call(connection: QueueEvent.connection),
|
|
25
33
|
event_count: QueueEvent.count,
|
|
26
34
|
max_size_bytes: SolidObserver.config.max_db_size
|
|
27
35
|
}
|
|
@@ -29,37 +37,38 @@ module SolidObserver
|
|
|
29
37
|
{error: "Failed to gather storage stats: #{e.message}"}
|
|
30
38
|
end
|
|
31
39
|
|
|
32
|
-
def calculate_database_size
|
|
33
|
-
db_config = QueueEvent.connection_db_config
|
|
34
|
-
db_path = db_config.database
|
|
35
|
-
|
|
36
|
-
return 0 unless File.exist?(db_path)
|
|
37
|
-
|
|
38
|
-
File.size(db_path)
|
|
39
|
-
rescue => e
|
|
40
|
-
warning("Could not calculate database size: #{e.message}")
|
|
41
|
-
0
|
|
42
|
-
end
|
|
43
|
-
|
|
44
40
|
def print_storage_table(stats)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
event_count = stats[:event_count]
|
|
42
|
+
db_size_bytes = stats[:db_size_bytes]
|
|
43
|
+
max_size_bytes = stats[:max_size_bytes]
|
|
48
44
|
|
|
49
45
|
table(
|
|
50
46
|
headers: ["Component", "Size", "Events", "Usage", "Status"],
|
|
51
|
-
rows: [
|
|
52
|
-
"Queue",
|
|
53
|
-
format_size(size_mb),
|
|
54
|
-
format_number(stats[:event_count]),
|
|
55
|
-
"#{percentage}%",
|
|
56
|
-
status
|
|
57
|
-
]]
|
|
47
|
+
rows: [storage_row(event_count: event_count, db_size_bytes: db_size_bytes, max_size_bytes: max_size_bytes)]
|
|
58
48
|
)
|
|
59
49
|
|
|
60
50
|
output("")
|
|
61
51
|
end
|
|
62
52
|
|
|
53
|
+
def storage_row(event_count:, db_size_bytes:, max_size_bytes:)
|
|
54
|
+
size, usage, status = storage_displays(db_size_bytes, max_size_bytes)
|
|
55
|
+
|
|
56
|
+
[
|
|
57
|
+
"Queue",
|
|
58
|
+
size,
|
|
59
|
+
format_number(event_count),
|
|
60
|
+
usage,
|
|
61
|
+
status
|
|
62
|
+
]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def storage_displays(db_size_bytes, max_size_bytes)
|
|
66
|
+
return ["N/A", "N/A", "— Unknown"] unless db_size_bytes
|
|
67
|
+
|
|
68
|
+
percentage = calculate_percentage(db_size_bytes, max_size_bytes)
|
|
69
|
+
[format_size(bytes_to_mb(db_size_bytes)), "#{percentage}%", status_indicator(percentage)]
|
|
70
|
+
end
|
|
71
|
+
|
|
63
72
|
def print_configuration
|
|
64
73
|
retention_days = (SolidObserver.config.event_retention / 1.day).to_i
|
|
65
74
|
max_size_mb = bytes_to_mb(SolidObserver.config.max_db_size)
|
|
@@ -105,8 +114,8 @@ module SolidObserver
|
|
|
105
114
|
|
|
106
115
|
def print_section_header(title)
|
|
107
116
|
output("")
|
|
108
|
-
output(title, color: :
|
|
109
|
-
output("=" * 50, color: :
|
|
117
|
+
output(title, color: :red)
|
|
118
|
+
output("=" * 50, color: :red)
|
|
110
119
|
output("")
|
|
111
120
|
end
|
|
112
121
|
end
|
|
@@ -15,14 +15,13 @@ module SolidObserver
|
|
|
15
15
|
# UI Settings
|
|
16
16
|
attr_accessor :ui_enabled,
|
|
17
17
|
:ui_base_controller,
|
|
18
|
-
:
|
|
19
|
-
:
|
|
20
|
-
:http_basic_auth_password
|
|
18
|
+
:ui_username,
|
|
19
|
+
:ui_password
|
|
21
20
|
|
|
22
21
|
# Observer Settings
|
|
23
22
|
attr_accessor :observe_queue
|
|
24
23
|
|
|
25
|
-
# Observer Settings (planned for
|
|
24
|
+
# Observer Settings (planned for a future release)
|
|
26
25
|
# @note Cache and Cable observers are not yet implemented
|
|
27
26
|
attr_accessor :observe_cache,
|
|
28
27
|
:observe_cable,
|
|
@@ -31,51 +30,58 @@ module SolidObserver
|
|
|
31
30
|
# Retention Settings
|
|
32
31
|
attr_accessor :event_retention
|
|
33
32
|
|
|
34
|
-
# Retention Settings (planned for
|
|
33
|
+
# Retention Settings (planned for a future release)
|
|
35
34
|
# @note Metrics cleanup is not yet implemented
|
|
36
35
|
attr_accessor :metrics_retention
|
|
37
36
|
|
|
38
37
|
# Storage Settings
|
|
39
38
|
attr_accessor :max_db_size
|
|
40
39
|
|
|
40
|
+
# Storage Mode
|
|
41
|
+
attr_reader :storage_mode
|
|
42
|
+
|
|
41
43
|
# Performance Settings (with validation)
|
|
42
44
|
attr_reader :sampling_rate,
|
|
43
45
|
:warning_threshold,
|
|
44
46
|
:buffer_size,
|
|
45
|
-
:flush_interval
|
|
47
|
+
:flush_interval,
|
|
48
|
+
:max_buffer_size,
|
|
49
|
+
:buffer_overflow_strategy,
|
|
50
|
+
:filter_cache_ttl
|
|
46
51
|
|
|
47
52
|
# Correlation Settings
|
|
48
53
|
attr_accessor :correlation_id_generator
|
|
49
54
|
|
|
50
55
|
def initialize
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
@
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
56
|
+
@ui_enabled, @ui_base_controller, @ui_username, @ui_password,
|
|
57
|
+
@storage_mode, @observe_queue, @observe_cache, @observe_cable,
|
|
58
|
+
@event_retention, @metrics_retention, @max_db_size, @warning_threshold,
|
|
59
|
+
@sampling_rate, @cache_sampling_rate, @buffer_size, @flush_interval,
|
|
60
|
+
@max_buffer_size, @buffer_overflow_strategy, @filter_cache_ttl,
|
|
61
|
+
@correlation_id_generator = !production?, "::ApplicationController", nil, nil,
|
|
62
|
+
:persistence, true, false, false,
|
|
63
|
+
30.days, 90.days, 1.gigabyte, 0.8,
|
|
64
|
+
1.0, 0.1, 1000, 10.seconds,
|
|
65
|
+
10_000, :drop_old, 1.minute,
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
STORAGE_MODES = %i[persistence realtime].freeze
|
|
70
|
+
BUFFER_OVERFLOW_STRATEGIES = %i[drop_old drop_new].freeze
|
|
71
|
+
|
|
72
|
+
def storage_mode=(value)
|
|
73
|
+
value = value.to_sym
|
|
74
|
+
raise ArgumentError, "storage_mode must be :persistence or :realtime" unless STORAGE_MODES.include?(value)
|
|
75
|
+
|
|
76
|
+
@storage_mode = value
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def persistence_mode?
|
|
80
|
+
@storage_mode == :persistence
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def realtime_mode?
|
|
84
|
+
@storage_mode == :realtime
|
|
79
85
|
end
|
|
80
86
|
|
|
81
87
|
def sampling_rate=(value)
|
|
@@ -90,6 +96,10 @@ module SolidObserver
|
|
|
90
96
|
|
|
91
97
|
def buffer_size=(value)
|
|
92
98
|
validate_positive_integer!(:buffer_size, value)
|
|
99
|
+
if defined?(@max_buffer_size) && value > @max_buffer_size
|
|
100
|
+
raise ArgumentError, "buffer_size must be <= max_buffer_size"
|
|
101
|
+
end
|
|
102
|
+
|
|
93
103
|
@buffer_size = value
|
|
94
104
|
end
|
|
95
105
|
|
|
@@ -98,6 +108,29 @@ module SolidObserver
|
|
|
98
108
|
@flush_interval = value
|
|
99
109
|
end
|
|
100
110
|
|
|
111
|
+
def max_buffer_size=(value)
|
|
112
|
+
validate_positive_integer!(:max_buffer_size, value)
|
|
113
|
+
if defined?(@buffer_size) && value < @buffer_size
|
|
114
|
+
raise ArgumentError, "max_buffer_size must be >= buffer_size"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
@max_buffer_size = value
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def buffer_overflow_strategy=(value)
|
|
121
|
+
value = value.to_sym
|
|
122
|
+
unless BUFFER_OVERFLOW_STRATEGIES.include?(value)
|
|
123
|
+
raise ArgumentError, "buffer_overflow_strategy must be :drop_old or :drop_new"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
@buffer_overflow_strategy = value
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def filter_cache_ttl=(value)
|
|
130
|
+
validate_positive_numeric!(:filter_cache_ttl, value)
|
|
131
|
+
@filter_cache_ttl = value
|
|
132
|
+
end
|
|
133
|
+
|
|
101
134
|
private
|
|
102
135
|
|
|
103
136
|
def validate_rate!(name, value)
|
|
@@ -38,12 +38,7 @@ module SolidObserver
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def call_custom_generator
|
|
41
|
-
|
|
42
|
-
return nil if result.blank?
|
|
43
|
-
result
|
|
44
|
-
rescue => e
|
|
45
|
-
log_generator_error(e)
|
|
46
|
-
nil
|
|
41
|
+
custom_generator_value.presence
|
|
47
42
|
end
|
|
48
43
|
|
|
49
44
|
def log_generator_error(exception)
|
|
@@ -58,5 +53,12 @@ module SolidObserver
|
|
|
58
53
|
def extract_job_id
|
|
59
54
|
@event.payload[:job].job_id
|
|
60
55
|
end
|
|
56
|
+
|
|
57
|
+
def custom_generator_value
|
|
58
|
+
SolidObserver.config.correlation_id_generator.call
|
|
59
|
+
rescue => e
|
|
60
|
+
log_generator_error(e)
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
61
63
|
end
|
|
62
64
|
end
|
|
@@ -4,6 +4,10 @@ module SolidObserver
|
|
|
4
4
|
class Engine < ::Rails::Engine
|
|
5
5
|
isolate_namespace SolidObserver
|
|
6
6
|
|
|
7
|
+
middleware.use ActionDispatch::Cookies
|
|
8
|
+
middleware.use ActionDispatch::Session::CookieStore, key: "_solid_observer_session"
|
|
9
|
+
middleware.use ActionDispatch::Flash
|
|
10
|
+
|
|
7
11
|
class << self
|
|
8
12
|
def check_solid_queue_availability
|
|
9
13
|
return if defined?(SolidQueue)
|
|
@@ -11,14 +15,35 @@ module SolidObserver
|
|
|
11
15
|
Rails.logger.warn "[SolidObserver] SolidQueue not detected. Queue observability features will be limited."
|
|
12
16
|
end
|
|
13
17
|
|
|
18
|
+
def check_ui_authentication
|
|
19
|
+
Services::UiAuthCheck.call(config: SolidObserver.config)
|
|
20
|
+
end
|
|
21
|
+
|
|
14
22
|
def configure_database_connection
|
|
15
|
-
|
|
23
|
+
return if SolidObserver.config.realtime_mode?
|
|
24
|
+
return unless queue_db_config
|
|
25
|
+
|
|
26
|
+
connect_observer_models
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def activate_subscribers
|
|
30
|
+
return activate_subscribers_in_realtime if SolidObserver.config.realtime_mode?
|
|
31
|
+
return if activation_skipped_for_table_status?
|
|
32
|
+
|
|
33
|
+
Rails.logger.info "[SolidObserver] Activating event subscribers"
|
|
34
|
+
Subscriber.subscribe!
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def queue_db_config
|
|
40
|
+
ActiveRecord::Base.configurations.configs_for(
|
|
16
41
|
env_name: Rails.env,
|
|
17
42
|
name: "solid_observer_queue"
|
|
18
43
|
)
|
|
44
|
+
end
|
|
19
45
|
|
|
20
|
-
|
|
21
|
-
|
|
46
|
+
def connect_observer_models
|
|
22
47
|
connection_config = {
|
|
23
48
|
database: {writing: :solid_observer_queue, reading: :solid_observer_queue}
|
|
24
49
|
}
|
|
@@ -27,24 +52,58 @@ module SolidObserver
|
|
|
27
52
|
SolidObserver::BaseMetric.connects_to(**connection_config)
|
|
28
53
|
end
|
|
29
54
|
|
|
30
|
-
def
|
|
31
|
-
logger
|
|
55
|
+
def activate_subscribers_in_realtime
|
|
56
|
+
Rails.logger.info "[SolidObserver] Starting in real-time mode (no persistence)"
|
|
57
|
+
Subscriber.subscribe!
|
|
58
|
+
end
|
|
32
59
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
60
|
+
def activation_skipped_for_table_status?
|
|
61
|
+
case table_status("solid_observer_queue_events")
|
|
62
|
+
when :absent
|
|
63
|
+
log_activation_skip("Tables not found. Run: rails solid_observer:install:migrations && rails db:migrate")
|
|
64
|
+
true
|
|
65
|
+
when :unknown
|
|
66
|
+
log_activation_skip("Database not reachable at boot. Skipping subscriber activation.")
|
|
67
|
+
true
|
|
68
|
+
else
|
|
69
|
+
false
|
|
36
70
|
end
|
|
71
|
+
end
|
|
37
72
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
rescue ActiveRecord::NoDatabaseError
|
|
41
|
-
logger.info "[SolidObserver] Database not ready yet. Skipping subscriber activation."
|
|
73
|
+
def log_activation_skip(message)
|
|
74
|
+
Rails.logger.info("[SolidObserver] #{message}")
|
|
42
75
|
end
|
|
43
76
|
|
|
44
|
-
|
|
77
|
+
def table_status(table_name)
|
|
78
|
+
pool = SolidObserver::BaseEvent.connection_pool
|
|
79
|
+
|
|
80
|
+
return :present if cached_data_source_exists?(pool, table_name)
|
|
81
|
+
|
|
82
|
+
data_source_exists_in_db?(pool, table_name) ? :present : :absent
|
|
83
|
+
rescue *boot_connection_errors
|
|
84
|
+
:unknown
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def cached_data_source_exists?(pool, table_name)
|
|
88
|
+
cache = pool.schema_cache
|
|
89
|
+
cache.data_source_exists?(pool, table_name)
|
|
90
|
+
rescue ArgumentError
|
|
91
|
+
cache.data_source_exists?(table_name)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def data_source_exists_in_db?(pool, table_name)
|
|
95
|
+
pool.with_connection { |connection| connection.data_source_exists?(table_name) }
|
|
96
|
+
end
|
|
45
97
|
|
|
46
|
-
def
|
|
47
|
-
|
|
98
|
+
def boot_connection_errors
|
|
99
|
+
[
|
|
100
|
+
ActiveRecord::NoDatabaseError,
|
|
101
|
+
ActiveRecord::ConnectionNotEstablished,
|
|
102
|
+
ActiveRecord::StatementInvalid,
|
|
103
|
+
*([PG::ConnectionBad] if defined?(PG::ConnectionBad)),
|
|
104
|
+
*([Mysql2::Error::ConnectionError] if defined?(Mysql2::Error::ConnectionError)),
|
|
105
|
+
*([SQLite3::CantOpenException] if defined?(SQLite3::CantOpenException))
|
|
106
|
+
]
|
|
48
107
|
end
|
|
49
108
|
end
|
|
50
109
|
|
|
@@ -55,6 +114,7 @@ module SolidObserver
|
|
|
55
114
|
config.after_initialize do
|
|
56
115
|
Engine.configure_database_connection
|
|
57
116
|
Engine.activate_subscribers
|
|
117
|
+
Engine.check_ui_authentication
|
|
58
118
|
end
|
|
59
119
|
end
|
|
60
120
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
module Params
|
|
5
|
+
class EventsFilter
|
|
6
|
+
def self.from_params(params)
|
|
7
|
+
new(
|
|
8
|
+
event_type: params[:event_type].presence,
|
|
9
|
+
job_class: params[:job_class].presence,
|
|
10
|
+
queue_name: params[:queue_name].presence,
|
|
11
|
+
from: parse_date(params[:from]),
|
|
12
|
+
to: parse_date(params[:to]),
|
|
13
|
+
page: (params[:page].presence || 1).to_i
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def parse_date(date_string)
|
|
21
|
+
return nil if date_string.blank?
|
|
22
|
+
|
|
23
|
+
Date.parse(date_string)
|
|
24
|
+
rescue ArgumentError
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
attr_reader :event_type, :job_class, :queue_name, :from, :to, :page
|
|
30
|
+
|
|
31
|
+
def initialize(event_type:, job_class:, queue_name:, from:, to:, page:)
|
|
32
|
+
@event_type, @job_class, @queue_name = event_type, job_class, queue_name
|
|
33
|
+
@from, @to, @page = from, to, page
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
module Params
|
|
5
|
+
class JobsFilter
|
|
6
|
+
ALLOWED_STATUSES = %w[ready scheduled claimed failed].freeze
|
|
7
|
+
PSEUDO_STATUSES = %w[all_active].freeze
|
|
8
|
+
|
|
9
|
+
def self.from_params(params)
|
|
10
|
+
new(
|
|
11
|
+
status: params[:status].presence || "all_active",
|
|
12
|
+
queue_name: params[:queue_name].presence,
|
|
13
|
+
job_class: params[:job_class].presence,
|
|
14
|
+
page: (params[:page].presence || 1).to_i
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :status, :queue_name, :job_class, :page
|
|
19
|
+
|
|
20
|
+
def initialize(status:, queue_name:, job_class:, page:)
|
|
21
|
+
@status = normalize_status(status)
|
|
22
|
+
@queue_name = queue_name
|
|
23
|
+
@job_class = job_class
|
|
24
|
+
@page = page
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def normalize_status(status)
|
|
30
|
+
normalized = status.to_s.downcase
|
|
31
|
+
(ALLOWED_STATUSES + PSEUDO_STATUSES).include?(normalized) ? normalized : "all_active"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|