solid_observer 0.3.0 → 0.5.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 +34 -0
- data/README.md +195 -82
- 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 +52 -0
- data/app/controllers/solid_observer/cache_operations_controller.rb +24 -0
- data/app/controllers/solid_observer/dashboard_controller.rb +38 -1
- data/app/controllers/solid_observer/storages_controller.rb +1 -1
- data/app/helpers/solid_observer/application_helper.rb +268 -5
- data/app/helpers/solid_observer/dashboard_helper.rb +30 -11
- 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_event.rb +15 -0
- data/app/models/solid_observer/cache_metric.rb +13 -0
- data/app/models/solid_observer/storage_info.rb +4 -1
- data/app/views/layouts/solid_observer/application.html.erb +157 -19
- data/app/views/solid_observer/cable_dashboard/_charts.html.erb +31 -0
- data/app/views/solid_observer/cable_dashboard/_recent_events.html.erb +34 -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/_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/_queue_table.html.erb +1 -0
- data/app/views/solid_observer/dashboard/index.html.erb +32 -5
- data/app/views/solid_observer/events/index.html.erb +1 -0
- data/app/views/solid_observer/jobs/index.html.erb +1 -0
- data/app/views/solid_observer/jobs/show.html.erb +3 -3
- data/app/views/solid_observer/storages/show.html.erb +90 -32
- data/config/routes.rb +7 -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/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/lib/generators/solid_observer/install_generator.rb +8 -1
- data/lib/generators/solid_observer/templates/initializer.rb.tt +20 -4
- 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 +28 -0
- data/lib/solid_observer/cache_metric_buffer.rb +229 -0
- data/lib/solid_observer/cache_subscriber.rb +47 -0
- data/lib/solid_observer/chart_buffer.rb +84 -27
- data/lib/solid_observer/cli/storage.rb +16 -13
- data/lib/solid_observer/configuration.rb +67 -5
- data/lib/solid_observer/engine.rb +70 -15
- data/lib/solid_observer/event_buffer_core.rb +218 -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_operations.rb +115 -0
- data/lib/solid_observer/services/cache_stats.rb +346 -0
- data/lib/solid_observer/services/cleanup_storage.rb +98 -47
- data/lib/solid_observer/services/database_size.rb +13 -8
- 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_event_buffer.rb +54 -0
- data/lib/solid_observer/services/flush_cache_metrics.rb +56 -0
- data/lib/solid_observer/services/record_cable_event.rb +114 -0
- data/lib/solid_observer/services/record_cable_metric.rb +73 -0
- data/lib/solid_observer/services/record_cache_event.rb +165 -0
- data/lib/solid_observer/services/record_cache_metric.rb +66 -0
- data/lib/solid_observer/services/storage_info_snapshot.rb +216 -0
- data/lib/solid_observer/version.rb +1 -1
- data/lib/solid_observer.rb +36 -11
- data/lib/tasks/solid_observer.rake +111 -21
- metadata +47 -5
- data/bin/console +0 -11
- data/bin/quality_gate +0 -95
- data/bin/setup +0 -8
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "dashboard_controller"
|
|
4
|
+
|
|
5
|
+
module SolidObserver
|
|
6
|
+
class CacheDashboardController < DashboardController
|
|
7
|
+
CACHE_STORAGE_COMPONENTS = %w[solid_cache cache_observer].freeze
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
@component = "cache"
|
|
11
|
+
assign_cache_dashboard
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def assign_cache_dashboard
|
|
17
|
+
unless SolidObserver.config.solid_cache_enabled?
|
|
18
|
+
@cache_dashboard_available = false
|
|
19
|
+
@storage_components = []
|
|
20
|
+
@recent_events = []
|
|
21
|
+
@activity_trends = SolidObserver::Services::CacheStats::ACTIVITY_TREND_EMPTY
|
|
22
|
+
@stability = SolidObserver::Services::CacheStats::STABILITY_EMPTY
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
range = SolidObserver::Services::CacheStats.parse_range(request_range_param)
|
|
27
|
+
window = SolidObserver::Services::CacheStats.range_duration(range)
|
|
28
|
+
stats = SolidObserver::Services::CacheStats.call(window: window)
|
|
29
|
+
|
|
30
|
+
@cache_dashboard_available = true
|
|
31
|
+
@range = range
|
|
32
|
+
@stats = stats
|
|
33
|
+
@activity_trends = stats[:activity_trends]
|
|
34
|
+
@stability = stats[:stability]
|
|
35
|
+
@storage_components = cache_storage_components
|
|
36
|
+
@recent_events = recent_events(window)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def cache_storage_components
|
|
40
|
+
SolidObserver::Services::StorageInfoSnapshot.call.select do |snapshot|
|
|
41
|
+
CACHE_STORAGE_COMPONENTS.include?(snapshot[:component])
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def recent_events(window)
|
|
46
|
+
current_time = Time.current
|
|
47
|
+
SolidObserver::CacheEvent.where(recorded_at: (current_time - window)..current_time).recent(10)
|
|
48
|
+
rescue ActiveRecord::StatementInvalid
|
|
49
|
+
[]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
class CacheOperationsController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@cache_controls_available = SolidObserver::Services::CacheOperations.available?
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def prune
|
|
10
|
+
redirect_with_result(SolidObserver::Services::CacheOperations.prune)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def clear
|
|
14
|
+
redirect_with_result(SolidObserver::Services::CacheOperations.clear)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def redirect_with_result(result)
|
|
20
|
+
flash_key = result[:ok] ? :notice : :alert
|
|
21
|
+
redirect_to cache_operations_path, flash_key => result[:message]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../../helpers/solid_observer/application_helper"
|
|
4
|
+
|
|
3
5
|
module SolidObserver
|
|
4
6
|
class DashboardController < ApplicationController
|
|
7
|
+
helper SolidObserver::ApplicationHelper
|
|
8
|
+
|
|
5
9
|
skip_forgery_protection only: :live_poll
|
|
6
10
|
skip_after_action :verify_same_origin_request, only: :live_poll
|
|
7
11
|
|
|
8
12
|
def index
|
|
13
|
+
@component = selected_component
|
|
14
|
+
|
|
15
|
+
return unless @component == "queue" && SolidObserver.config.solid_queue_enabled?
|
|
16
|
+
|
|
9
17
|
assign_range_and_stats
|
|
10
18
|
load_persistence_data if persistence_mode?
|
|
11
19
|
end
|
|
12
20
|
|
|
13
21
|
def live_poll
|
|
22
|
+
expires_in 1.day, public: true
|
|
14
23
|
send_file(
|
|
15
24
|
SolidObserver::Engine.root.join("app/assets/javascripts/solid_observer/live_poll.js"),
|
|
16
25
|
type: "application/javascript; charset=utf-8",
|
|
@@ -32,7 +41,13 @@ module SolidObserver
|
|
|
32
41
|
@range = range
|
|
33
42
|
@live = request_live_param == "on"
|
|
34
43
|
@stats = QueueStats.snapshot(range: range)
|
|
35
|
-
@chart =
|
|
44
|
+
@chart = if @stats[:available]
|
|
45
|
+
QueueStats.chart_data(window: QueueStats.range_duration(@range))
|
|
46
|
+
else
|
|
47
|
+
{performed: [], failed: [], ready: []}
|
|
48
|
+
end
|
|
49
|
+
rescue
|
|
50
|
+
@chart = {performed: [], failed: [], ready: []}
|
|
36
51
|
end
|
|
37
52
|
|
|
38
53
|
def load_persistence_data
|
|
@@ -75,5 +90,27 @@ module SolidObserver
|
|
|
75
90
|
def append_chart_buffer
|
|
76
91
|
ChartBuffer.append(SolidQueue::ReadyExecution.count) if QueueStats.solid_queue_available?
|
|
77
92
|
end
|
|
93
|
+
|
|
94
|
+
def selected_component
|
|
95
|
+
requested = if request&.respond_to?(:path_parameters)
|
|
96
|
+
request.path_parameters&.[](:component).to_s
|
|
97
|
+
else
|
|
98
|
+
""
|
|
99
|
+
end
|
|
100
|
+
requested = path_component if requested.empty?
|
|
101
|
+
return "cache" if requested == "cache" && SolidObserver.config.solid_cache_enabled?
|
|
102
|
+
|
|
103
|
+
"queue"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def path_component
|
|
107
|
+
return "" unless request&.respond_to?(:path)
|
|
108
|
+
|
|
109
|
+
path = request&.path.to_s
|
|
110
|
+
return "cache" if path.end_with?("/cache")
|
|
111
|
+
return "queue" if path.end_with?("/queue")
|
|
112
|
+
|
|
113
|
+
""
|
|
114
|
+
end
|
|
78
115
|
end
|
|
79
116
|
end
|
|
@@ -5,7 +5,7 @@ module SolidObserver
|
|
|
5
5
|
include RequirePersistenceMode
|
|
6
6
|
|
|
7
7
|
def show
|
|
8
|
-
@
|
|
8
|
+
@storage_components = SolidObserver::Services::StorageInfoSnapshot.call
|
|
9
9
|
@storage_history = SolidObserver::StorageInfo.recent(20)
|
|
10
10
|
end
|
|
11
11
|
end
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "dashboard_helper"
|
|
4
|
+
|
|
3
5
|
module SolidObserver
|
|
4
6
|
module ApplicationHelper
|
|
7
|
+
include SolidObserver::DashboardHelper
|
|
8
|
+
|
|
5
9
|
STATUS_COLORS = {
|
|
6
10
|
"completed" => "success",
|
|
7
11
|
"ready" => "success",
|
|
@@ -23,6 +27,21 @@ module SolidObserver
|
|
|
23
27
|
degraded: {label: "Degraded", tone: "warning"},
|
|
24
28
|
critical: {label: "Critical", tone: "danger"}
|
|
25
29
|
}.freeze
|
|
30
|
+
CACHE_OUTCOME_STATES = {
|
|
31
|
+
hit: {label: "Hit", tone: "success"},
|
|
32
|
+
miss: {label: "Miss", tone: "info"},
|
|
33
|
+
error: {label: "Error", tone: "danger"},
|
|
34
|
+
recorded: {label: "Recorded", tone: "recorded"}
|
|
35
|
+
}.freeze
|
|
36
|
+
CACHE_RANGE_LABELS = {
|
|
37
|
+
"15m" => "in last 15m",
|
|
38
|
+
"30m" => "in last 30m",
|
|
39
|
+
"1h" => "in last hour",
|
|
40
|
+
"7h" => "in last 7h",
|
|
41
|
+
"1d" => "in last day",
|
|
42
|
+
"7d" => "in last 7d",
|
|
43
|
+
"14d" => "in last 14d"
|
|
44
|
+
}.freeze
|
|
26
45
|
|
|
27
46
|
def execution_status(execution)
|
|
28
47
|
ExecutionPresenter.new(execution).status
|
|
@@ -41,7 +60,15 @@ module SolidObserver
|
|
|
41
60
|
def status_badge(status)
|
|
42
61
|
status_str = status.to_s
|
|
43
62
|
color = STATUS_COLORS.fetch(status_str, "default")
|
|
44
|
-
|
|
63
|
+
dot = tag.svg(
|
|
64
|
+
tag.circle(r: 3, cx: 3, cy: 3),
|
|
65
|
+
class: "so-badge__dot",
|
|
66
|
+
viewBox: "0 0 6 6",
|
|
67
|
+
"aria-hidden": "true"
|
|
68
|
+
)
|
|
69
|
+
tag.span(class: "so-badge so-badge--pill so-badge--#{color}") do
|
|
70
|
+
safe_join([dot, status_str.humanize], " ")
|
|
71
|
+
end
|
|
45
72
|
end
|
|
46
73
|
|
|
47
74
|
def duration_with_semantic(value, event_type)
|
|
@@ -53,7 +80,15 @@ module SolidObserver
|
|
|
53
80
|
def mode_badge
|
|
54
81
|
config = SolidObserver.config
|
|
55
82
|
color = config.persistence_mode? ? "info" : "warning"
|
|
56
|
-
|
|
83
|
+
dot = tag.svg(
|
|
84
|
+
tag.circle(r: 3, cx: 3, cy: 3),
|
|
85
|
+
class: "so-badge__dot",
|
|
86
|
+
viewBox: "0 0 6 6",
|
|
87
|
+
"aria-hidden": "true"
|
|
88
|
+
)
|
|
89
|
+
tag.span(class: "so-badge so-badge--pill so-badge--#{color}") do
|
|
90
|
+
safe_join([dot, config.storage_mode.to_s.capitalize], " ")
|
|
91
|
+
end
|
|
57
92
|
end
|
|
58
93
|
|
|
59
94
|
def turbo_frame_tag(id, **options, &block)
|
|
@@ -73,14 +108,125 @@ module SolidObserver
|
|
|
73
108
|
end
|
|
74
109
|
|
|
75
110
|
def stability_badge(stats)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
111
|
+
stability_badge_for(stability_state(stats))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def cache_stability_badge(state)
|
|
115
|
+
stability_badge_for(state.to_sym)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def cache_ratio_percent(value)
|
|
119
|
+
number_to_percentage(value.to_f * 100, precision: 1, strip_insignificant_zeros: true)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def cache_storage_summary(storage_components)
|
|
123
|
+
snapshots = Array(storage_components)
|
|
124
|
+
reason = cache_storage_unavailable_reason(snapshots)
|
|
125
|
+
return {value: "—", subtitle: "— #{reason}"} if reason
|
|
126
|
+
|
|
127
|
+
{
|
|
128
|
+
value: number_to_human_size(cache_storage_total_bytes(snapshots), precision: 1, significant: false, strip_insignificant_zeros: false),
|
|
129
|
+
subtitle: "SolidCache + cache observer"
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def cache_event_outcome_badge(event)
|
|
134
|
+
meta = cache_event_outcome_meta(event)
|
|
135
|
+
dot = tag.svg(
|
|
136
|
+
tag.circle(r: 3, cx: 3, cy: 3),
|
|
137
|
+
class: "so-badge__dot",
|
|
138
|
+
viewBox: "0 0 6 6",
|
|
139
|
+
"aria-hidden": "true"
|
|
140
|
+
)
|
|
141
|
+
|
|
79
142
|
tag.span(class: "so-badge so-badge--pill so-badge--#{meta[:tone]}") do
|
|
80
143
|
safe_join([dot, meta[:label]], " ")
|
|
81
144
|
end
|
|
82
145
|
end
|
|
83
146
|
|
|
147
|
+
def cache_event_digest(key_digest, visible_chars: 10)
|
|
148
|
+
digest = key_digest.to_s
|
|
149
|
+
return "—" if digest.empty?
|
|
150
|
+
return digest if digest.length <= visible_chars
|
|
151
|
+
|
|
152
|
+
"#{digest.first(visible_chars)}…"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def cache_range_label(range_key)
|
|
156
|
+
CACHE_RANGE_LABELS.fetch(range_key.to_s, "in selected range")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def cable_range_label(range_key)
|
|
160
|
+
CACHE_RANGE_LABELS.fetch(range_key.to_s, "in selected range")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def cable_ratio_percent(value)
|
|
164
|
+
number_to_percentage(value.to_f * 100, precision: 1, strip_insignificant_zeros: true)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def cable_stability_badge(state)
|
|
168
|
+
stability_badge_for(state.to_sym)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def cable_stability_detail(stability)
|
|
172
|
+
state = (stability || {})[:state]&.to_sym
|
|
173
|
+
state = :stable unless STABILITY_STATES.key?(state)
|
|
174
|
+
|
|
175
|
+
case state
|
|
176
|
+
when :critical
|
|
177
|
+
critical_cable_stability_detail(stability)
|
|
178
|
+
when :degraded
|
|
179
|
+
degraded_cable_stability_detail(stability)
|
|
180
|
+
else
|
|
181
|
+
"No cable errors or subscription rejections in the selected range and backlog current snapshot is healthy"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def cable_event_digest(digest, visible_chars: 10)
|
|
186
|
+
digest = digest.to_s
|
|
187
|
+
return "—" if digest.empty?
|
|
188
|
+
return digest if digest.length <= visible_chars
|
|
189
|
+
|
|
190
|
+
"#{digest.first(visible_chars)}…"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# :reek:FeatureEnvy
|
|
194
|
+
def cable_backlog_summary(stats)
|
|
195
|
+
if stats[:backlog_available]
|
|
196
|
+
{
|
|
197
|
+
value: number_with_delimiter(stats[:backlog_count].to_i),
|
|
198
|
+
subtitle: "current Solid Cable snapshot"
|
|
199
|
+
}
|
|
200
|
+
else
|
|
201
|
+
{value: "—", subtitle: "current Solid Cable snapshot unavailable"}
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def cable_storage_summary(storage_components)
|
|
206
|
+
snapshots = Array(storage_components)
|
|
207
|
+
reason = cable_storage_unavailable_reason(snapshots)
|
|
208
|
+
return {value: "—", subtitle: "— #{reason}"} if reason
|
|
209
|
+
|
|
210
|
+
{
|
|
211
|
+
value: number_to_human_size(cable_storage_total_bytes(snapshots), precision: 1, significant: false, strip_insignificant_zeros: false),
|
|
212
|
+
subtitle: "Cable telemetry + Solid Cable messages"
|
|
213
|
+
}
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def cache_stability_detail(stability)
|
|
217
|
+
state = (stability || {})[:state]&.to_sym
|
|
218
|
+
state = :stable unless STABILITY_STATES.key?(state)
|
|
219
|
+
|
|
220
|
+
case state
|
|
221
|
+
when :critical
|
|
222
|
+
critical_cache_stability_detail(stability)
|
|
223
|
+
when :degraded
|
|
224
|
+
degraded_cache_stability_detail(stability)
|
|
225
|
+
else
|
|
226
|
+
"No sampled cache errors or slow events in the selected range"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
84
230
|
def stability_detail(stats)
|
|
85
231
|
failures_24h = stats[:failed_last_24h].to_i
|
|
86
232
|
return "No failures in the last 24h" if failures_24h.zero?
|
|
@@ -91,5 +237,122 @@ module SolidObserver
|
|
|
91
237
|
def latest_failure_phrase(timestamp)
|
|
92
238
|
timestamp ? "#{time_ago_in_words(timestamp)} ago" : "unknown"
|
|
93
239
|
end
|
|
240
|
+
|
|
241
|
+
def queue_component_enabled?
|
|
242
|
+
SolidObserver.config.solid_queue_enabled?
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def cache_component_enabled?
|
|
246
|
+
SolidObserver.config.solid_cache_enabled?
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def cable_component_enabled?
|
|
250
|
+
SolidObserver.config.solid_cable_enabled?
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def dashboard_section_active?(component)
|
|
254
|
+
current_component = @component.presence || "queue"
|
|
255
|
+
controller_name == "dashboard" && current_component == component.to_s
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
private
|
|
259
|
+
|
|
260
|
+
def stability_badge_for(state)
|
|
261
|
+
meta = STABILITY_STATES.fetch(state)
|
|
262
|
+
dot = tag.svg(tag.circle(r: 3, cx: 3, cy: 3),
|
|
263
|
+
class: "so-badge__dot", viewBox: "0 0 6 6", "aria-hidden": "true")
|
|
264
|
+
tag.span(class: "so-badge so-badge--pill so-badge--#{meta[:tone]}") do
|
|
265
|
+
safe_join([dot, meta[:label]], " ")
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def critical_cache_stability_detail(stability)
|
|
270
|
+
detail = pluralize(stability[:error_count].to_i, "sampled cache error")
|
|
271
|
+
slow_count = stability[:slow_count].to_i
|
|
272
|
+
detail = "#{detail} and #{pluralize(slow_count, "slow event")}" if slow_count.positive?
|
|
273
|
+
"#{detail} in the selected range#{cache_stability_latest_suffix(stability[:latest_recorded_at])}"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def degraded_cache_stability_detail(stability)
|
|
277
|
+
detail = pluralize(stability[:slow_count].to_i, "slow sampled cache event")
|
|
278
|
+
"#{detail} in the selected range#{cache_stability_latest_suffix(stability[:latest_recorded_at])}"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def cache_stability_latest_suffix(timestamp)
|
|
282
|
+
timestamp ? ", latest #{time_ago_in_words(timestamp)} ago" : ""
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def cache_storage_total_bytes(snapshots)
|
|
286
|
+
snapshots.sum { |snapshot| snapshot[:db_size_bytes].to_i }
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def cache_storage_unavailable_reason(snapshots)
|
|
290
|
+
return "Storage snapshot unavailable" unless snapshots.size == 2
|
|
291
|
+
|
|
292
|
+
snapshots.find { |snapshot| !snapshot[:available] }&.[](:unavailable_reason)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# :reek:FeatureEnvy
|
|
296
|
+
# :reek:TooManyStatements
|
|
297
|
+
def critical_cable_stability_detail(stability)
|
|
298
|
+
parts = []
|
|
299
|
+
error_count = stability[:error_count].to_i
|
|
300
|
+
parts << pluralize(error_count, "cable error") if error_count.positive?
|
|
301
|
+
|
|
302
|
+
rejection_count = stability[:rejection_count].to_i
|
|
303
|
+
rejection_rate = stability[:rejection_rate].to_f
|
|
304
|
+
if rejection_rate > 0.0
|
|
305
|
+
parts << "#{cable_ratio_percent(rejection_rate)} rejection rate"
|
|
306
|
+
elsif rejection_count.positive?
|
|
307
|
+
parts << pluralize(rejection_count, "subscription rejection")
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
backlog_ratio = stability[:backlog_ratio].to_f
|
|
311
|
+
if backlog_ratio >= 0.5
|
|
312
|
+
parts << "backlog at #{number_to_percentage(backlog_ratio * 100, precision: 0)} in current snapshot"
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
return "Cable stability critical" if parts.empty?
|
|
316
|
+
|
|
317
|
+
"#{parts.join("; ")} in the selected range"
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# :reek:FeatureEnvy
|
|
321
|
+
# :reek:TooManyStatements
|
|
322
|
+
def degraded_cable_stability_detail(stability)
|
|
323
|
+
return "Backlog current snapshot unavailable" unless stability[:backlog_available]
|
|
324
|
+
|
|
325
|
+
backlog_ratio = stability[:backlog_ratio].to_f
|
|
326
|
+
if backlog_ratio >= SolidObserver.config.cable_backlog_threshold.to_f
|
|
327
|
+
return "Backlog at #{number_to_percentage(backlog_ratio * 100, precision: 0)} in current snapshot"
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
rejection_count = stability[:rejection_count].to_i
|
|
331
|
+
if rejection_count.positive?
|
|
332
|
+
return "#{pluralize(rejection_count, "subscription rejection")} in the selected range"
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
"Cable stability degraded"
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def cable_storage_total_bytes(snapshots)
|
|
339
|
+
snapshots.sum { |snapshot| snapshot[:db_size_bytes].to_i }
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def cable_storage_unavailable_reason(snapshots)
|
|
343
|
+
return "Storage snapshot unavailable" unless snapshots.size == 2
|
|
344
|
+
|
|
345
|
+
snapshots.find { |snapshot| !snapshot[:available] }&.[](:unavailable_reason)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def cache_event_outcome_meta(event)
|
|
349
|
+
hit = event.hit
|
|
350
|
+
|
|
351
|
+
return CACHE_OUTCOME_STATES.fetch(:error) if event.error_class.present?
|
|
352
|
+
return CACHE_OUTCOME_STATES.fetch(:hit) if hit == true
|
|
353
|
+
return CACHE_OUTCOME_STATES.fetch(:miss) if hit == false
|
|
354
|
+
|
|
355
|
+
CACHE_OUTCOME_STATES.fetch(:recorded)
|
|
356
|
+
end
|
|
94
357
|
end
|
|
95
358
|
end
|
|
@@ -9,17 +9,36 @@ module SolidObserver
|
|
|
9
9
|
def spark_points(series, width: SVG_W, height: SVG_H)
|
|
10
10
|
return "" if series.blank?
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
series.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
context = build_spark_context(series, width, height)
|
|
13
|
+
series.map { |point| format_spark_point(point: point, context: context) }.join(" ")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def build_spark_context(series, width, height)
|
|
17
|
+
first_point = series.first
|
|
18
|
+
last_point = series.last
|
|
19
|
+
min_time = first_point[:t]
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
min_time: min_time,
|
|
23
|
+
time_span: last_point[:t] - min_time,
|
|
24
|
+
max_value: [series.max_by { |point| point[:v] }[:v], 1].max,
|
|
25
|
+
width: width,
|
|
26
|
+
height: height,
|
|
27
|
+
inner_width: width - 2,
|
|
28
|
+
inner_height: height - 2
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def format_spark_point(point:, context:)
|
|
33
|
+
time_span = context[:time_span]
|
|
34
|
+
coordinate_x = if time_span.zero?
|
|
35
|
+
context[:width] / 2.0
|
|
36
|
+
else
|
|
37
|
+
((point[:t] - context[:min_time]).to_f / time_span) * context[:inner_width] + 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
coordinate_y = context[:height] - 1 - (point[:v].to_f / context[:max_value]) * context[:inner_height]
|
|
41
|
+
format("%.1f,%.1f", coordinate_x, coordinate_y)
|
|
23
42
|
end
|
|
24
43
|
|
|
25
44
|
RANGE_LABELS = {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
class CableEvent < BaseEvent
|
|
5
|
+
self.table_name = "solid_observer_cable_events"
|
|
6
|
+
|
|
7
|
+
validates :event_type, presence: true
|
|
8
|
+
validates :recorded_at, presence: true
|
|
9
|
+
|
|
10
|
+
scope :errored, -> { where.not(error_class: nil) }
|
|
11
|
+
scope :recent, ->(limit = 10) { order(recorded_at: :desc).limit(limit) }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
class CableMetric < BaseRecord
|
|
5
|
+
self.table_name = "solid_observer_cable_metrics"
|
|
6
|
+
|
|
7
|
+
validates :period_start, presence: true
|
|
8
|
+
validates :broadcasts_count, :transmissions_count, :confirmations_count,
|
|
9
|
+
:rejections_count, :perform_actions_count, :errors_count,
|
|
10
|
+
numericality: {only_integer: true, greater_than_or_equal_to: 0}
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
class CacheEvent < BaseEvent
|
|
5
|
+
self.table_name = "solid_observer_cache_events"
|
|
6
|
+
|
|
7
|
+
validates :event_type, presence: true
|
|
8
|
+
validates :key_digest, presence: true
|
|
9
|
+
validates :recorded_at, presence: true
|
|
10
|
+
|
|
11
|
+
scope :errored, -> { where.not(error_class: nil) }
|
|
12
|
+
scope :slow, ->(threshold = SolidObserver.config.cache_slow_threshold) { where("duration >= ?", threshold) }
|
|
13
|
+
scope :recent, ->(limit = 10) { order(recorded_at: :desc).limit(limit) }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
class CacheMetric < BaseRecord
|
|
5
|
+
self.table_name = "solid_observer_cache_metrics"
|
|
6
|
+
|
|
7
|
+
validates :event_type, presence: true, length: {maximum: 64}
|
|
8
|
+
validates :period_start, presence: true
|
|
9
|
+
validates :operations_count, :hits_count, :misses_count, :errors_count,
|
|
10
|
+
numericality: {only_integer: true, greater_than_or_equal_to: 0}
|
|
11
|
+
validates :duration_total, numericality: {greater_than_or_equal_to: 0}
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -6,16 +6,19 @@ module SolidObserver
|
|
|
6
6
|
|
|
7
7
|
MB_TO_BYTES = 1_048_576
|
|
8
8
|
GB_TO_BYTES = 1_073_741_824
|
|
9
|
+
COMPONENTS = %w[queue_observer cache_observer solid_cache cable_observer solid_cable].freeze
|
|
9
10
|
|
|
10
11
|
validates :db_size_bytes, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
|
|
11
12
|
validates :event_count, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
|
|
12
13
|
validates :recorded_at, presence: true
|
|
14
|
+
validates :component, presence: true, inclusion: {in: COMPONENTS}
|
|
13
15
|
|
|
14
16
|
scope :recent, ->(limit = 10) { order(recorded_at: :desc).limit(limit) }
|
|
15
17
|
scope :since, ->(time) { where("recorded_at >= ?", time) }
|
|
16
18
|
|
|
17
|
-
def self.record_snapshot(db_size:, event_count:)
|
|
19
|
+
def self.record_snapshot(db_size:, event_count:, component: "queue_observer")
|
|
18
20
|
create!(
|
|
21
|
+
component: component,
|
|
19
22
|
db_size_bytes: db_size || 0,
|
|
20
23
|
event_count: event_count,
|
|
21
24
|
recorded_at: Time.current
|