solid_observer 0.4.0 → 0.6.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 +30 -0
- data/README.md +100 -25
- data/app/assets/javascripts/solid_observer/live_poll.js +3 -1
- data/app/controllers/solid_observer/application_controller.rb +1 -0
- data/app/controllers/solid_observer/cable_dashboard_controller.rb +52 -0
- data/app/controllers/solid_observer/cable_operations_controller.rb +16 -0
- data/app/controllers/solid_observer/cache_dashboard_controller.rb +33 -40
- data/app/controllers/solid_observer/dashboard_controller.rb +21 -13
- data/app/controllers/solid_observer/traces_controller.rb +17 -0
- data/app/helpers/solid_observer/application_helper.rb +145 -3
- data/app/models/solid_observer/cable_event.rb +13 -0
- data/app/models/solid_observer/cable_metric.rb +12 -0
- data/app/models/solid_observer/cache_metric.rb +1 -2
- data/app/models/solid_observer/storage_info.rb +1 -1
- data/app/views/layouts/solid_observer/application.html.erb +35 -9
- data/app/views/solid_observer/cable_dashboard/_charts.html.erb +31 -0
- data/app/views/solid_observer/cable_dashboard/_recent_events.html.erb +39 -0
- data/app/views/solid_observer/cable_dashboard/_summary.html.erb +34 -0
- data/app/views/solid_observer/cable_dashboard/index.html.erb +118 -0
- data/app/views/solid_observer/cache_dashboard/_recent_events.html.erb +6 -1
- data/app/views/solid_observer/dashboard/_component_cards.html.erb +18 -0
- data/app/views/solid_observer/dashboard/_health.html.erb +6 -0
- data/app/views/solid_observer/dashboard/_home.html.erb +10 -0
- data/app/views/solid_observer/dashboard/_queue.html.erb +138 -0
- data/app/views/solid_observer/dashboard/_queue_table.html.erb +1 -0
- data/app/views/solid_observer/dashboard/_unified_feed.html.erb +38 -0
- data/app/views/solid_observer/dashboard/index.html.erb +3 -139
- data/app/views/solid_observer/events/index.html.erb +7 -1
- data/app/views/solid_observer/jobs/index.html.erb +1 -0
- data/app/views/solid_observer/storages/show.html.erb +29 -3
- data/app/views/solid_observer/traces/show.html.erb +80 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20260612000001_add_event_type_recorded_at_index_to_cache_events.rb +21 -0
- data/db/migrate/20260619000001_create_solid_observer_cable_events.rb +22 -0
- data/db/migrate/20260619000002_create_solid_observer_cable_metrics.rb +17 -0
- data/db/migrate/20260630000001_add_correlation_id_to_cache_events.rb +8 -0
- data/db/migrate/20260630000002_add_correlation_id_to_cable_events.rb +8 -0
- data/lib/generators/solid_observer/install_generator.rb +8 -1
- data/lib/generators/solid_observer/templates/initializer.rb.tt +24 -5
- data/lib/solid_observer/base_event.rb +1 -1
- data/lib/solid_observer/base_metric.rb +1 -1
- data/lib/solid_observer/base_record.rb +8 -0
- data/lib/solid_observer/cable_event_buffer.rb +28 -0
- data/lib/solid_observer/cable_metric_buffer.rb +230 -0
- data/lib/solid_observer/cable_subscriber.rb +57 -0
- data/lib/solid_observer/cache_event_buffer.rb +11 -36
- data/lib/solid_observer/cache_metric_buffer.rb +229 -0
- data/lib/solid_observer/chart_buffer.rb +84 -27
- data/lib/solid_observer/cli/health.rb +49 -0
- data/lib/solid_observer/cli/trace.rb +106 -0
- data/lib/solid_observer/configuration.rb +47 -7
- data/lib/solid_observer/correlated.rb +23 -0
- data/lib/solid_observer/engine.rb +46 -28
- data/lib/solid_observer/event_buffer_core.rb +218 -0
- data/lib/solid_observer/queries/trace_query.rb +151 -0
- data/lib/solid_observer/queue_event_buffer.rb +9 -201
- data/lib/solid_observer/services/cable_operations.rb +74 -0
- data/lib/solid_observer/services/cable_stats.rb +385 -0
- data/lib/solid_observer/services/cache_stats.rb +35 -18
- data/lib/solid_observer/services/cleanup_storage.rb +82 -47
- data/lib/solid_observer/services/flush_cable_event_buffer.rb +54 -0
- data/lib/solid_observer/services/flush_cable_metrics.rb +54 -0
- data/lib/solid_observer/services/flush_cache_metrics.rb +56 -0
- data/lib/solid_observer/services/health_score.rb +85 -0
- data/lib/solid_observer/services/record_cable_event.rb +115 -0
- data/lib/solid_observer/services/record_cable_metric.rb +73 -0
- data/lib/solid_observer/services/record_cache_event.rb +24 -0
- data/lib/solid_observer/services/record_cache_metric.rb +13 -21
- data/lib/solid_observer/services/storage_info_snapshot.rb +103 -15
- data/lib/solid_observer/services/unified_feed.rb +101 -0
- data/lib/solid_observer/version.rb +1 -1
- data/lib/solid_observer.rb +42 -11
- data/lib/tasks/solid_observer.rake +97 -21
- metadata +41 -6
- data/app/assets/stylesheets/solid_observer/application.css +0 -18
- data/bin/console +0 -11
- data/bin/quality_gate +0 -95
- data/bin/setup +0 -8
|
@@ -2,7 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidObserver
|
|
4
4
|
class ChartBuffer
|
|
5
|
+
CACHE_KEY = "solid_observer/chart_buffer/ready_samples"
|
|
5
6
|
INSTANCE_MUTEX = Mutex.new
|
|
7
|
+
SAMPLE_CAP = 720
|
|
8
|
+
STORAGE_MUTEX = Mutex.new
|
|
9
|
+
|
|
10
|
+
class SampleWindow
|
|
11
|
+
def initialize(samples)
|
|
12
|
+
@samples = samples
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def upsert(sample, cap:)
|
|
16
|
+
return replace_latest_sample(sample) if latest_sample_timestamp == sample[:t]
|
|
17
|
+
|
|
18
|
+
append_new_sample(sample, cap: cap)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def latest_sample_timestamp
|
|
24
|
+
@samples.last&.[](:t)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def replace_latest_sample(sample)
|
|
28
|
+
@samples[-1] = sample
|
|
29
|
+
@samples
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def append_new_sample(sample, cap:)
|
|
33
|
+
@samples << sample
|
|
34
|
+
overflow = @samples.length - cap
|
|
35
|
+
@samples.shift(overflow) if overflow.positive?
|
|
36
|
+
@samples
|
|
37
|
+
end
|
|
38
|
+
end
|
|
6
39
|
|
|
7
40
|
class << self
|
|
8
41
|
def append(value, at: Time.now)
|
|
@@ -24,16 +57,12 @@ module SolidObserver
|
|
|
24
57
|
end
|
|
25
58
|
end
|
|
26
59
|
|
|
27
|
-
|
|
28
|
-
@mutex = Mutex.new
|
|
29
|
-
@samples = []
|
|
30
|
-
@cap = nil
|
|
31
|
-
end
|
|
60
|
+
@fallback_samples = []
|
|
32
61
|
|
|
33
62
|
def append(value, at: Time.now)
|
|
34
63
|
sample = {t: at.to_i, v: value.to_i}
|
|
35
64
|
|
|
36
|
-
|
|
65
|
+
STORAGE_MUTEX.synchronize { persist_sample(sample) }
|
|
37
66
|
|
|
38
67
|
sample
|
|
39
68
|
end
|
|
@@ -41,43 +70,71 @@ module SolidObserver
|
|
|
41
70
|
def recent(window_seconds)
|
|
42
71
|
cutoff = Time.now.to_i - window_seconds.to_i
|
|
43
72
|
|
|
44
|
-
|
|
45
|
-
|
|
73
|
+
STORAGE_MUTEX.synchronize do
|
|
74
|
+
load_samples.select { |sample| sample[:t] >= cutoff }.map(&:dup)
|
|
46
75
|
end
|
|
47
76
|
end
|
|
48
77
|
|
|
49
78
|
def clear
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
79
|
+
STORAGE_MUTEX.synchronize do
|
|
80
|
+
empty_samples = []
|
|
81
|
+
replace_fallback_samples(empty_samples)
|
|
82
|
+
cache_store&.delete(CACHE_KEY)
|
|
83
|
+
rescue
|
|
84
|
+
nil
|
|
53
85
|
end
|
|
54
86
|
end
|
|
55
87
|
|
|
56
88
|
private
|
|
57
89
|
|
|
58
|
-
def
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
trim_to_cap
|
|
90
|
+
def persist_sample(sample)
|
|
91
|
+
samples = SampleWindow.new(load_samples).upsert(sample, cap: SAMPLE_CAP)
|
|
92
|
+
write_samples(samples)
|
|
62
93
|
end
|
|
63
94
|
|
|
64
|
-
def
|
|
65
|
-
|
|
95
|
+
def load_samples
|
|
96
|
+
normalize_samples(cache_store&.read(CACHE_KEY) || stored_fallback_samples)
|
|
97
|
+
rescue
|
|
98
|
+
stored_fallback_samples
|
|
99
|
+
end
|
|
66
100
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
101
|
+
def write_samples(samples)
|
|
102
|
+
normalized = normalize_samples(samples)
|
|
103
|
+
|
|
104
|
+
replace_fallback_samples(normalized.map(&:dup))
|
|
105
|
+
cache_store&.write(CACHE_KEY, normalized)
|
|
106
|
+
rescue
|
|
107
|
+
replace_fallback_samples(normalized)
|
|
72
108
|
end
|
|
73
109
|
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
|
|
110
|
+
def cache_store
|
|
111
|
+
return unless defined?(Rails)
|
|
112
|
+
|
|
113
|
+
Rails.cache
|
|
77
114
|
end
|
|
78
115
|
|
|
79
|
-
def
|
|
80
|
-
(
|
|
116
|
+
def stored_fallback_samples
|
|
117
|
+
normalize_samples(self.class.instance_variable_get(:@fallback_samples))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def replace_fallback_samples(samples)
|
|
121
|
+
self.class.instance_variable_set(:@fallback_samples, samples)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def normalize_samples(samples)
|
|
125
|
+
Array(samples).filter_map do |sample|
|
|
126
|
+
normalize_sample(sample)
|
|
127
|
+
end.last(SAMPLE_CAP)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def normalize_sample(sample)
|
|
131
|
+
return unless sample.is_a?(Hash)
|
|
132
|
+
|
|
133
|
+
timestamp = sample[:t] || sample["t"]
|
|
134
|
+
value = sample[:v] || sample["v"]
|
|
135
|
+
return unless timestamp && value
|
|
136
|
+
|
|
137
|
+
{t: timestamp.to_i, v: value.to_i}
|
|
81
138
|
end
|
|
82
139
|
end
|
|
83
140
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
module CLI
|
|
5
|
+
class Health < Base
|
|
6
|
+
STATUS_COLORS = {stable: :green, degraded: :yellow, critical: :red}.freeze
|
|
7
|
+
NAME_WIDTH = 8
|
|
8
|
+
UNAVAILABLE_SUFFIX = " (unavailable)"
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
health = SolidObserver::Services::HealthScore.call
|
|
12
|
+
|
|
13
|
+
print_header
|
|
14
|
+
print_overall(health[:overall])
|
|
15
|
+
print_components(health[:components])
|
|
16
|
+
output("")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def print_header
|
|
22
|
+
output("\n🩺 SolidObserver Health", color: :red)
|
|
23
|
+
output("=" * 50, color: :red)
|
|
24
|
+
output("")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def print_overall(overall)
|
|
28
|
+
output("Overall: #{overall}", color: STATUS_COLORS[overall])
|
|
29
|
+
output("")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def print_components(components)
|
|
33
|
+
return warning("⚠️ No components enabled") if components.empty?
|
|
34
|
+
|
|
35
|
+
output("Components", color: :green)
|
|
36
|
+
output("")
|
|
37
|
+
components.each { |name, entry| print_component(name, entry[:status], entry[:available]) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Renders the status verbatim: HealthScore owns the worst-of ladder, so an
|
|
41
|
+
# unrecognised status stays uncoloured rather than being coerced to stable.
|
|
42
|
+
# :reek:ControlParameter
|
|
43
|
+
def print_component(name, status, available)
|
|
44
|
+
suffix = available ? "" : UNAVAILABLE_SUFFIX
|
|
45
|
+
output(" #{name.to_s.ljust(NAME_WIDTH)}#{status}#{suffix}", color: STATUS_COLORS[status])
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
module CLI
|
|
5
|
+
class Trace < Base
|
|
6
|
+
DETAIL_FORMATTERS = {
|
|
7
|
+
queue: :format_queue_details,
|
|
8
|
+
cache: :format_cache_details,
|
|
9
|
+
cable: :format_cable_details
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
EVENT_FORMATTERS = {
|
|
13
|
+
"cable_summary" => ->(row) { "and #{row[:collapsed_count]} more cable events" }
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def call(correlation_id:, limit: nil)
|
|
17
|
+
return output("Trace unavailable in realtime mode.") if SolidObserver.config.realtime_mode?
|
|
18
|
+
|
|
19
|
+
result = SolidObserver::Queries::TraceQuery.new.call(correlation_id: correlation_id, limit: limit)
|
|
20
|
+
return warning("No events found for correlation_id: #{correlation_id}") if (rows = result.rows).empty?
|
|
21
|
+
|
|
22
|
+
print_trace_table(rows)
|
|
23
|
+
print_unavailable_components(result.unavailable_components)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def print_trace_table(rows)
|
|
29
|
+
print_section_header("Trace")
|
|
30
|
+
table(
|
|
31
|
+
headers: ["Time", "Component", "Event", "Details"],
|
|
32
|
+
rows: rows.map { |row| format_cells(row) }
|
|
33
|
+
)
|
|
34
|
+
output("")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def print_unavailable_components(unavailable)
|
|
38
|
+
unavailable.each { |component| warning("#{component} unavailable") }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def format_cells(row)
|
|
42
|
+
recorded_at, component, event_type = row.values_at(:recorded_at, :component, :event_type)
|
|
43
|
+
[
|
|
44
|
+
format_time(recorded_at),
|
|
45
|
+
component.to_s,
|
|
46
|
+
event_type,
|
|
47
|
+
format_details(row)
|
|
48
|
+
]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def format_details(row)
|
|
52
|
+
event_type, component = row.values_at(:event_type, :component)
|
|
53
|
+
event_formatter = EVENT_FORMATTERS[event_type]
|
|
54
|
+
return event_formatter.call(row) if event_formatter
|
|
55
|
+
|
|
56
|
+
component_formatter = DETAIL_FORMATTERS[component]
|
|
57
|
+
send(component_formatter, row) if component_formatter
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def format_queue_details(row)
|
|
61
|
+
job_class, queue_name, duration = row.values_at(:job_class, :queue_name, :duration)
|
|
62
|
+
[label("job", job_class), label("queue", queue_name), duration_part(duration)].compact.join(" ")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def format_cache_details(row)
|
|
66
|
+
hit, error_class, duration = row.values_at(:hit, :error_class, :duration)
|
|
67
|
+
[hit_part(hit), error_part(error_class), duration_part(duration)].compact.join(" ")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def format_cable_details(row)
|
|
71
|
+
channel_class, duration, error_class, collapsed_count = row.values_at(:channel_class, :duration, :error_class, :collapsed_count)
|
|
72
|
+
[label("channel", channel_class), error_part(error_class), duration_part(duration), ("collapsed=#{collapsed_count}" if collapsed_count)].compact.join(" ")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def label(name, value)
|
|
76
|
+
"#{name}=#{value}" if value
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def error_part(error_class)
|
|
80
|
+
"error=#{error_class}" if error_class
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def hit_part(hit)
|
|
84
|
+
"hit=#{hit}" if [true, false].include?(hit)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def duration_part(duration)
|
|
88
|
+
"duration=#{format_duration(duration)}" if duration
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def format_duration(duration)
|
|
92
|
+
duration ? "#{duration.round(3)}s" : nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def format_time(time)
|
|
96
|
+
time.strftime("%Y-%m-%d %H:%M:%S")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def print_section_header(title)
|
|
100
|
+
output("\n#{title}", color: :red)
|
|
101
|
+
output("=" * 50, color: :red)
|
|
102
|
+
output("")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -21,19 +21,20 @@ module SolidObserver
|
|
|
21
21
|
# Observer Settings
|
|
22
22
|
attr_accessor :observe_queue
|
|
23
23
|
|
|
24
|
-
#
|
|
25
|
-
# @note Cache and Cable observers are not yet implemented
|
|
24
|
+
# Cache & Cable Observer Settings
|
|
26
25
|
attr_accessor :observe_cache,
|
|
27
26
|
:observe_cable,
|
|
28
27
|
:cache_sampling_rate,
|
|
29
28
|
:cache_slow_threshold,
|
|
30
29
|
:cache_store_errors
|
|
31
30
|
|
|
31
|
+
attr_reader :cable_rejection_threshold,
|
|
32
|
+
:cable_backlog_threshold,
|
|
33
|
+
:cable_error_threshold
|
|
34
|
+
|
|
32
35
|
# Retention Settings
|
|
33
36
|
attr_accessor :event_retention
|
|
34
37
|
|
|
35
|
-
# Retention Settings (planned for a future release)
|
|
36
|
-
# @note Metrics cleanup is not yet implemented
|
|
37
38
|
attr_accessor :metrics_retention
|
|
38
39
|
|
|
39
40
|
# Storage Settings
|
|
@@ -49,7 +50,8 @@ module SolidObserver
|
|
|
49
50
|
:flush_interval,
|
|
50
51
|
:max_buffer_size,
|
|
51
52
|
:buffer_overflow_strategy,
|
|
52
|
-
:filter_cache_ttl
|
|
53
|
+
:filter_cache_ttl,
|
|
54
|
+
:cable_sampling_rate
|
|
53
55
|
|
|
54
56
|
# Correlation Settings
|
|
55
57
|
attr_accessor :correlation_id_generator
|
|
@@ -58,13 +60,16 @@ module SolidObserver
|
|
|
58
60
|
@ui_enabled, @ui_base_controller, @ui_username, @ui_password,
|
|
59
61
|
@storage_mode, @observe_queue, @observe_cache, @observe_cable,
|
|
60
62
|
@event_retention, @metrics_retention, @max_db_size, @warning_threshold,
|
|
61
|
-
@sampling_rate, @cache_sampling_rate, @cache_slow_threshold, @cache_store_errors,
|
|
63
|
+
@sampling_rate, @cache_sampling_rate, @cable_sampling_rate, @cache_slow_threshold, @cache_store_errors,
|
|
64
|
+
@cable_rejection_threshold, @cable_backlog_threshold, @cable_error_threshold,
|
|
62
65
|
@buffer_size, @flush_interval,
|
|
63
66
|
@max_buffer_size, @buffer_overflow_strategy, @filter_cache_ttl,
|
|
64
67
|
@correlation_id_generator = !production?, "::ApplicationController", nil, nil,
|
|
65
68
|
:persistence, true, false, false,
|
|
66
69
|
30.days, 90.days, 1.gigabyte, 0.8,
|
|
67
|
-
1.0, 0.1, 0.1,
|
|
70
|
+
1.0, 0.1, 0.1, 0.1, true,
|
|
71
|
+
0.05, 0.10, 0.0,
|
|
72
|
+
1000, 10.seconds,
|
|
68
73
|
10_000, :drop_old, 1.minute,
|
|
69
74
|
nil
|
|
70
75
|
end
|
|
@@ -95,6 +100,10 @@ module SolidObserver
|
|
|
95
100
|
!!defined?(::SolidCache)
|
|
96
101
|
end
|
|
97
102
|
|
|
103
|
+
def solid_cable_available?
|
|
104
|
+
!!defined?(::SolidCable)
|
|
105
|
+
end
|
|
106
|
+
|
|
98
107
|
def solid_queue_enabled?
|
|
99
108
|
observe_queue
|
|
100
109
|
end
|
|
@@ -103,11 +112,35 @@ module SolidObserver
|
|
|
103
112
|
observe_cache && solid_cache_available?
|
|
104
113
|
end
|
|
105
114
|
|
|
115
|
+
def solid_cable_enabled?
|
|
116
|
+
observe_cable && solid_cable_available?
|
|
117
|
+
end
|
|
118
|
+
|
|
106
119
|
def sampling_rate=(value)
|
|
107
120
|
validate_rate!(:sampling_rate, value)
|
|
108
121
|
@sampling_rate = value
|
|
109
122
|
end
|
|
110
123
|
|
|
124
|
+
def cable_sampling_rate=(value)
|
|
125
|
+
validate_rate!(:cable_sampling_rate, value)
|
|
126
|
+
@cable_sampling_rate = value
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def cable_rejection_threshold=(value)
|
|
130
|
+
validate_rate!(:cable_rejection_threshold, value)
|
|
131
|
+
@cable_rejection_threshold = value
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def cable_backlog_threshold=(value)
|
|
135
|
+
validate_rate!(:cable_backlog_threshold, value)
|
|
136
|
+
@cable_backlog_threshold = value
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def cable_error_threshold=(value)
|
|
140
|
+
validate_non_negative_numeric!(:cable_error_threshold, value)
|
|
141
|
+
@cable_error_threshold = value
|
|
142
|
+
end
|
|
143
|
+
|
|
111
144
|
def warning_threshold=(value)
|
|
112
145
|
validate_rate!(:warning_threshold, value)
|
|
113
146
|
@warning_threshold = value
|
|
@@ -170,6 +203,13 @@ module SolidObserver
|
|
|
170
203
|
end
|
|
171
204
|
end
|
|
172
205
|
|
|
206
|
+
# :reek:FeatureEnvy
|
|
207
|
+
def validate_non_negative_numeric!(name, value)
|
|
208
|
+
unless value.is_a?(Numeric) && value >= 0
|
|
209
|
+
raise ArgumentError, "#{name} must be a non-negative number"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
173
213
|
def production?
|
|
174
214
|
defined?(Rails) && Rails.env.production?
|
|
175
215
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
module Correlated
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.around_perform do |job, block|
|
|
7
|
+
SolidObserver::Correlated.stamp(job.job_id) { block.call }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.stamp(correlation_id)
|
|
12
|
+
thread_local = Thread.current
|
|
13
|
+
previous = thread_local[:solid_observer_correlation_id]
|
|
14
|
+
thread_local[:solid_observer_correlation_id] = correlation_id if correlation_id.present?
|
|
15
|
+
|
|
16
|
+
yield
|
|
17
|
+
ensure
|
|
18
|
+
thread_local[:solid_observer_correlation_id] = previous
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
ActiveSupport.on_load(:active_job) { include SolidObserver::Correlated }
|
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "cache_event_buffer"
|
|
4
|
-
require_relative "cache_subscriber"
|
|
5
|
-
require_relative "services/record_cache_event"
|
|
6
|
-
require_relative "services/record_cache_metric"
|
|
7
|
-
require_relative "services/flush_cache_event_buffer"
|
|
8
|
-
require_relative "services/cache_stats"
|
|
9
|
-
require_relative "services/cache_operations"
|
|
10
|
-
|
|
11
3
|
module SolidObserver
|
|
12
4
|
class Engine < ::Rails::Engine
|
|
13
5
|
isolate_namespace SolidObserver
|
|
14
6
|
|
|
15
|
-
SOLID_QUEUE_AVAILABLE = defined?(::SolidQueue)
|
|
16
|
-
SOLID_CACHE_AVAILABLE = defined?(::SolidCache)
|
|
17
|
-
|
|
18
7
|
middleware.use ActionDispatch::Cookies
|
|
19
8
|
middleware.use ActionDispatch::Session::CookieStore, key: "_solid_observer_session"
|
|
20
9
|
middleware.use ActionDispatch::Flash
|
|
@@ -33,6 +22,13 @@ module SolidObserver
|
|
|
33
22
|
Rails.logger.warn "[SolidObserver] SolidCache not detected. Cache observability features will be disabled."
|
|
34
23
|
end
|
|
35
24
|
|
|
25
|
+
def check_solid_cable_availability
|
|
26
|
+
return if defined?(::SolidCable)
|
|
27
|
+
return unless SolidObserver.config.observe_cable
|
|
28
|
+
|
|
29
|
+
Rails.logger.warn "[SolidObserver] SolidCable not detected. Cable observability features will be disabled."
|
|
30
|
+
end
|
|
31
|
+
|
|
36
32
|
def check_ui_authentication
|
|
37
33
|
Services::UiAuthCheck.call(config: SolidObserver.config)
|
|
38
34
|
end
|
|
@@ -46,16 +42,18 @@ module SolidObserver
|
|
|
46
42
|
|
|
47
43
|
def activate_subscribers
|
|
48
44
|
return activate_subscribers_in_realtime if SolidObserver.config.realtime_mode?
|
|
49
|
-
return if activation_skipped_for_table_status_for_enabled_components?
|
|
50
45
|
|
|
51
46
|
Rails.logger.info "[SolidObserver] Activating event subscribers"
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
activate_queue_subscriber
|
|
48
|
+
activate_cache_subscriber
|
|
49
|
+
activate_cable_subscriber
|
|
54
50
|
end
|
|
55
51
|
|
|
56
52
|
private
|
|
57
53
|
|
|
58
54
|
def queue_db_config
|
|
55
|
+
return unless active_record_available?
|
|
56
|
+
|
|
59
57
|
ActiveRecord::Base.configurations.configs_for(
|
|
60
58
|
env_name: Rails.env,
|
|
61
59
|
name: "solid_observer_queue"
|
|
@@ -67,34 +65,40 @@ module SolidObserver
|
|
|
67
65
|
database: {writing: :solid_observer_queue, reading: :solid_observer_queue}
|
|
68
66
|
}
|
|
69
67
|
|
|
70
|
-
SolidObserver::
|
|
71
|
-
|
|
68
|
+
SolidObserver::BaseRecord.connects_to(**connection_config)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def active_record_available?
|
|
72
|
+
defined?(::ActiveRecord::Base)
|
|
72
73
|
end
|
|
73
74
|
|
|
74
75
|
def activate_subscribers_in_realtime
|
|
75
76
|
Rails.logger.info "[SolidObserver] Starting in real-time mode (no persistence)"
|
|
76
77
|
Subscriber.subscribe! if should_activate_queue_subscriber?
|
|
77
78
|
CacheSubscriber.subscribe! if should_activate_cache_subscriber?
|
|
79
|
+
CableSubscriber.subscribe! if should_activate_cable_subscriber?
|
|
78
80
|
end
|
|
79
81
|
|
|
80
|
-
def
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
enabled_tables << "solid_observer_cache_events" if should_activate_cache_subscriber?
|
|
82
|
+
def activate_queue_subscriber
|
|
83
|
+
activate_subscriber_for_table("solid_observer_queue_events", Subscriber) if should_activate_queue_subscriber?
|
|
84
|
+
end
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
def activate_cache_subscriber
|
|
87
|
+
activate_subscriber_for_table("solid_observer_cache_events", CacheSubscriber) if should_activate_cache_subscriber?
|
|
86
88
|
end
|
|
87
89
|
|
|
88
|
-
def
|
|
90
|
+
def activate_cable_subscriber
|
|
91
|
+
activate_subscriber_for_table("solid_observer_cable_events", CableSubscriber) if should_activate_cable_subscriber?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def activate_subscriber_for_table(table_name, subscriber)
|
|
89
95
|
case table_status(table_name)
|
|
90
96
|
when :absent
|
|
91
97
|
log_activation_skip("Tables not found (missing: #{table_name}). Run: rails solid_observer:install:migrations && rails db:migrate")
|
|
92
|
-
true
|
|
93
98
|
when :unknown
|
|
94
99
|
log_activation_skip("Database not reachable at boot. Skipping subscriber activation.")
|
|
95
|
-
true
|
|
96
100
|
else
|
|
97
|
-
|
|
101
|
+
subscriber.subscribe!
|
|
98
102
|
end
|
|
99
103
|
end
|
|
100
104
|
|
|
@@ -106,18 +110,28 @@ module SolidObserver
|
|
|
106
110
|
SolidObserver.config.solid_cache_enabled?
|
|
107
111
|
end
|
|
108
112
|
|
|
113
|
+
def should_activate_cable_subscriber?
|
|
114
|
+
SolidObserver.config.solid_cable_enabled?
|
|
115
|
+
end
|
|
116
|
+
|
|
109
117
|
def log_activation_skip(message)
|
|
110
118
|
Rails.logger.info("[SolidObserver] #{message}")
|
|
111
119
|
end
|
|
112
120
|
|
|
113
121
|
def table_status(table_name)
|
|
114
|
-
|
|
122
|
+
return :unknown unless active_record_available?
|
|
123
|
+
|
|
124
|
+
data_source_status(table_name)
|
|
125
|
+
rescue *boot_connection_errors
|
|
126
|
+
:unknown
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def data_source_status(table_name)
|
|
130
|
+
pool = SolidObserver::BaseRecord.connection_pool
|
|
115
131
|
|
|
116
132
|
return :present if cached_data_source_exists?(pool, table_name)
|
|
117
133
|
|
|
118
134
|
data_source_exists_in_db?(pool, table_name) ? :present : :absent
|
|
119
|
-
rescue *boot_connection_errors
|
|
120
|
-
:unknown
|
|
121
135
|
end
|
|
122
136
|
|
|
123
137
|
def cached_data_source_exists?(pool, table_name)
|
|
@@ -132,12 +146,15 @@ module SolidObserver
|
|
|
132
146
|
end
|
|
133
147
|
|
|
134
148
|
def boot_connection_errors
|
|
149
|
+
return [] unless active_record_available?
|
|
150
|
+
|
|
135
151
|
[
|
|
136
152
|
ActiveRecord::NoDatabaseError,
|
|
137
153
|
ActiveRecord::ConnectionNotEstablished,
|
|
138
154
|
ActiveRecord::StatementInvalid,
|
|
139
155
|
*([PG::ConnectionBad] if defined?(PG::ConnectionBad)),
|
|
140
156
|
*([Mysql2::Error::ConnectionError] if defined?(Mysql2::Error::ConnectionError)),
|
|
157
|
+
*([Trilogy::Error] if defined?(Trilogy::Error)),
|
|
141
158
|
*([SQLite3::CantOpenException] if defined?(SQLite3::CantOpenException))
|
|
142
159
|
]
|
|
143
160
|
end
|
|
@@ -146,6 +163,7 @@ module SolidObserver
|
|
|
146
163
|
config.before_initialize do
|
|
147
164
|
Engine.check_solid_queue_availability
|
|
148
165
|
Engine.check_solid_cache_availability
|
|
166
|
+
Engine.check_solid_cable_availability
|
|
149
167
|
end
|
|
150
168
|
|
|
151
169
|
config.after_initialize do
|