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
|
@@ -156,6 +156,63 @@ module SolidObserver
|
|
|
156
156
|
CACHE_RANGE_LABELS.fetch(range_key.to_s, "in selected range")
|
|
157
157
|
end
|
|
158
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
|
+
|
|
159
216
|
def cache_stability_detail(stability)
|
|
160
217
|
state = (stability || {})[:state]&.to_sym
|
|
161
218
|
state = :stable unless STABILITY_STATES.key?(state)
|
|
@@ -189,9 +246,41 @@ module SolidObserver
|
|
|
189
246
|
SolidObserver.config.solid_cache_enabled?
|
|
190
247
|
end
|
|
191
248
|
|
|
192
|
-
def
|
|
193
|
-
|
|
194
|
-
|
|
249
|
+
def cable_component_enabled?
|
|
250
|
+
SolidObserver.config.solid_cable_enabled?
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def home?
|
|
254
|
+
controller_name == "dashboard" && @component.to_s == "home"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def health_status_badge(status)
|
|
258
|
+
stability_badge_for(status.to_sym)
|
|
259
|
+
rescue KeyError
|
|
260
|
+
Rails.logger&.warn("health_status_badge: unknown status #{status.inspect}, falling back to degraded")
|
|
261
|
+
stability_badge_for(:degraded)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def unified_feed_detail(row)
|
|
265
|
+
detail = row[:detail].to_s
|
|
266
|
+
if row[:correlation_id].present?
|
|
267
|
+
link_to(detail.presence || row[:correlation_id], trace_path(row[:correlation_id]), class: "so-trace__link")
|
|
268
|
+
else
|
|
269
|
+
detail.presence || "—"
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def component_overview_path(name)
|
|
274
|
+
case name.to_sym
|
|
275
|
+
when :queue
|
|
276
|
+
queue_dashboard_path
|
|
277
|
+
when :cache
|
|
278
|
+
cache_dashboard_path
|
|
279
|
+
when :cable
|
|
280
|
+
cable_dashboard_path
|
|
281
|
+
else
|
|
282
|
+
root_path
|
|
283
|
+
end
|
|
195
284
|
end
|
|
196
285
|
|
|
197
286
|
private
|
|
@@ -231,6 +320,59 @@ module SolidObserver
|
|
|
231
320
|
snapshots.find { |snapshot| !snapshot[:available] }&.[](:unavailable_reason)
|
|
232
321
|
end
|
|
233
322
|
|
|
323
|
+
# :reek:FeatureEnvy
|
|
324
|
+
# :reek:TooManyStatements
|
|
325
|
+
def critical_cable_stability_detail(stability)
|
|
326
|
+
parts = []
|
|
327
|
+
error_count = stability[:error_count].to_i
|
|
328
|
+
parts << pluralize(error_count, "cable error") if error_count.positive?
|
|
329
|
+
|
|
330
|
+
rejection_count = stability[:rejection_count].to_i
|
|
331
|
+
rejection_rate = stability[:rejection_rate].to_f
|
|
332
|
+
if rejection_rate > 0.0
|
|
333
|
+
parts << "#{cable_ratio_percent(rejection_rate)} rejection rate"
|
|
334
|
+
elsif rejection_count.positive?
|
|
335
|
+
parts << pluralize(rejection_count, "subscription rejection")
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
backlog_ratio = stability[:backlog_ratio].to_f
|
|
339
|
+
if backlog_ratio >= 0.5
|
|
340
|
+
parts << "backlog at #{number_to_percentage(backlog_ratio * 100, precision: 0)} in current snapshot"
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
return "Cable stability critical" if parts.empty?
|
|
344
|
+
|
|
345
|
+
"#{parts.join("; ")} in the selected range"
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# :reek:FeatureEnvy
|
|
349
|
+
# :reek:TooManyStatements
|
|
350
|
+
def degraded_cable_stability_detail(stability)
|
|
351
|
+
return "Backlog current snapshot unavailable" unless stability[:backlog_available]
|
|
352
|
+
|
|
353
|
+
backlog_ratio = stability[:backlog_ratio].to_f
|
|
354
|
+
if backlog_ratio >= SolidObserver.config.cable_backlog_threshold.to_f
|
|
355
|
+
return "Backlog at #{number_to_percentage(backlog_ratio * 100, precision: 0)} in current snapshot"
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
rejection_count = stability[:rejection_count].to_i
|
|
359
|
+
if rejection_count.positive?
|
|
360
|
+
return "#{pluralize(rejection_count, "subscription rejection")} in the selected range"
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
"Cable stability degraded"
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def cable_storage_total_bytes(snapshots)
|
|
367
|
+
snapshots.sum { |snapshot| snapshot[:db_size_bytes].to_i }
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def cable_storage_unavailable_reason(snapshots)
|
|
371
|
+
return "Storage snapshot unavailable" unless snapshots.size == 2
|
|
372
|
+
|
|
373
|
+
snapshots.find { |snapshot| !snapshot[:available] }&.[](:unavailable_reason)
|
|
374
|
+
end
|
|
375
|
+
|
|
234
376
|
def cache_event_outcome_meta(event)
|
|
235
377
|
hit = event.hit
|
|
236
378
|
|
|
@@ -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
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SolidObserver
|
|
4
|
-
class CacheMetric <
|
|
4
|
+
class CacheMetric < BaseRecord
|
|
5
5
|
self.table_name = "solid_observer_cache_metrics"
|
|
6
|
-
clear_validators!
|
|
7
6
|
|
|
8
7
|
validates :event_type, presence: true, length: {maximum: 64}
|
|
9
8
|
validates :period_start, presence: true
|
|
@@ -6,7 +6,7 @@ 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].freeze
|
|
9
|
+
COMPONENTS = %w[queue_observer cache_observer solid_cache cable_observer solid_cable].freeze
|
|
10
10
|
|
|
11
11
|
validates :db_size_bytes, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
|
|
12
12
|
validates :event_count, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
/* text */
|
|
16
16
|
--so-text: #171717;
|
|
17
|
-
--so-text-muted: #
|
|
18
|
-
--so-text-subtle: #
|
|
17
|
+
--so-text-muted: #707070;
|
|
18
|
+
--so-text-subtle: #707070;
|
|
19
19
|
|
|
20
20
|
/* lines */
|
|
21
21
|
--so-border: #e5e5e5;
|
|
@@ -398,7 +398,8 @@
|
|
|
398
398
|
.so-cache-controls { max-width: 820px; }
|
|
399
399
|
.so-cache-dashboard__intro,
|
|
400
400
|
.so-cache-controls__intro,
|
|
401
|
-
.so-queue-overview__intro
|
|
401
|
+
.so-queue-overview__intro,
|
|
402
|
+
.so-cable-dashboard__intro {
|
|
402
403
|
display: flex;
|
|
403
404
|
flex-wrap: wrap;
|
|
404
405
|
align-items: center;
|
|
@@ -407,16 +408,19 @@
|
|
|
407
408
|
}
|
|
408
409
|
.so-cache-dashboard__hint,
|
|
409
410
|
.so-cache-controls__hint,
|
|
410
|
-
.so-queue-overview__hint
|
|
411
|
+
.so-queue-overview__hint,
|
|
412
|
+
.so-cable-dashboard__hint {
|
|
411
413
|
font-size: 0.875rem;
|
|
412
414
|
color: var(--so-text-subtle);
|
|
413
415
|
line-height: 1.5;
|
|
414
416
|
}
|
|
415
|
-
.so-cache-dashboard__range-form
|
|
417
|
+
.so-cache-dashboard__range-form,
|
|
418
|
+
.so-cable-dashboard__range-form {
|
|
416
419
|
align-items: center;
|
|
417
420
|
margin-bottom: 2rem;
|
|
418
421
|
}
|
|
419
422
|
.so-cache-dashboard__range-form select,
|
|
423
|
+
.so-cable-dashboard__range-form select,
|
|
420
424
|
.so-dashboard-toolbar select {
|
|
421
425
|
padding: 0.4rem 0.6rem;
|
|
422
426
|
border: 1px solid var(--so-border);
|
|
@@ -425,11 +429,13 @@
|
|
|
425
429
|
background: var(--so-card-bg);
|
|
426
430
|
color: var(--so-text);
|
|
427
431
|
}
|
|
428
|
-
.so-cache-dashboard__chart-empty
|
|
432
|
+
.so-cache-dashboard__chart-empty,
|
|
433
|
+
.so-cable-dashboard__chart-empty {
|
|
429
434
|
max-width: 420px;
|
|
430
435
|
margin-bottom: 0;
|
|
431
436
|
}
|
|
432
|
-
.so-cache-dashboard__digest
|
|
437
|
+
.so-cache-dashboard__digest,
|
|
438
|
+
.so-cable-dashboard__digest {
|
|
433
439
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
434
440
|
white-space: nowrap;
|
|
435
441
|
}
|
|
@@ -518,6 +524,19 @@
|
|
|
518
524
|
|
|
519
525
|
.so-card li + li { margin-top: 0.45rem; }
|
|
520
526
|
|
|
527
|
+
/* SO-098 trace page */
|
|
528
|
+
.so-trace { max-width: 1120px; }
|
|
529
|
+
.so-trace__meta { margin-top: .35rem; font-size: .875rem; line-height: 1.5; color: var(--so-text-subtle); }
|
|
530
|
+
.so-trace__correlation { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; overflow-wrap: anywhere; }
|
|
531
|
+
.so-trace__table { min-width: 760px; }
|
|
532
|
+
.so-trace__time { width: 10.75rem; white-space: nowrap; }
|
|
533
|
+
.so-trace__component { width: 7.75rem; white-space: nowrap; }
|
|
534
|
+
.so-trace__event { width: 10.5rem; white-space: nowrap; }
|
|
535
|
+
.so-trace__details { min-width: 20rem; white-space: normal; overflow-wrap: anywhere; line-height: 1.5; }
|
|
536
|
+
.so-trace__link { color: var(--so-info); text-decoration: none; }
|
|
537
|
+
.so-trace__link:hover { text-decoration: underline; }
|
|
538
|
+
.so-trace__link:focus-visible { outline: 2px solid var(--so-info); outline-offset: 2px; box-shadow: 0 0 0 3px var(--so-focus-ring); }
|
|
539
|
+
|
|
521
540
|
@media (max-width: 960px) {
|
|
522
541
|
.so-dashboard-toolbar { flex-direction: column; align-items: stretch; }
|
|
523
542
|
.so-dashboard-toolbar__left, .so-dashboard-toolbar__right { margin-left: 0; }
|
|
@@ -547,9 +566,11 @@
|
|
|
547
566
|
<aside class="so-sidebar">
|
|
548
567
|
<div class="so-sidebar__logo">SolidObserver</div>
|
|
549
568
|
<nav class="so-sidebar__nav" aria-label="SolidObserver navigation">
|
|
569
|
+
<%= link_to "Home", root_path, class: ("active" if home?), "aria-current": (home? ? "page" : nil) %>
|
|
570
|
+
|
|
550
571
|
<% if SolidObserver.config.solid_queue_enabled? %>
|
|
551
572
|
<div class="so-sidebar__section">Queue</div>
|
|
552
|
-
<%= link_to "Overview",
|
|
573
|
+
<%= link_to "Overview", queue_dashboard_path, class: ("active" if controller_name == "dashboard" && @component.to_s == "queue"), "aria-current": (controller_name == "dashboard" && @component.to_s == "queue" ? "page" : nil) %>
|
|
553
574
|
<%= link_to "Jobs", jobs_path, class: ("active" if controller_name == "jobs"), "aria-current": (controller_name == "jobs" ? "page" : nil) %>
|
|
554
575
|
<% if persistence_mode? %>
|
|
555
576
|
<%= link_to "Events", events_path, class: ("active" if controller_name == "events"), "aria-current": (controller_name == "events" ? "page" : nil) %>
|
|
@@ -564,7 +585,12 @@
|
|
|
564
585
|
<% end %>
|
|
565
586
|
<% end %>
|
|
566
587
|
|
|
567
|
-
<% if
|
|
588
|
+
<% if SolidObserver.config.solid_cable_enabled? %>
|
|
589
|
+
<div class="so-sidebar__section">Cable</div>
|
|
590
|
+
<%= link_to "Overview", cable_dashboard_path, class: ("active" if controller_name == "cable_dashboard"), "aria-current": (controller_name == "cable_dashboard" ? "page" : nil) %>
|
|
591
|
+
<% end %>
|
|
592
|
+
|
|
593
|
+
<% if persistence_mode? && (SolidObserver.config.solid_queue_enabled? || SolidObserver.config.solid_cache_enabled? || SolidObserver.config.solid_cable_enabled?) %>
|
|
568
594
|
<%= link_to "Storage", storage_path, class: ("active" if controller_name == "storages"), "aria-current": (controller_name == "storages" ? "page" : nil) %>
|
|
569
595
|
<% end %>
|
|
570
596
|
</nav>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<section class="so-dashboard-section" aria-labelledby="so-cable-charts-heading">
|
|
2
|
+
<header class="so-dashboard-section__header">
|
|
3
|
+
<h2 id="so-cable-charts-heading" class="so-dashboard-section__title">Activity trends</h2>
|
|
4
|
+
</header>
|
|
5
|
+
|
|
6
|
+
<div class="so-chart-strip">
|
|
7
|
+
<% if @stats[:activity_trends]&.[](:available) %>
|
|
8
|
+
<figure class="so-spark" data-so-spark="cable-broadcasts">
|
|
9
|
+
<figcaption class="so-spark__label">Broadcasts total <span data-so-range-copy><%= cable_range_label(@range) %></span></figcaption>
|
|
10
|
+
<svg class="so-spark__svg" viewBox="0 0 120 32" preserveAspectRatio="none" aria-hidden="true">
|
|
11
|
+
<line class="so-spark__baseline" x1="1" x2="119" y1="31" y2="31"/>
|
|
12
|
+
<polyline class="so-spark__line" points="<%= spark_points(@stats[:activity_trends][:broadcasts]) %>"/>
|
|
13
|
+
</svg>
|
|
14
|
+
<span class="so-spark__value"><%= number_with_delimiter(@stats[:broadcasts_count].to_i) %></span>
|
|
15
|
+
</figure>
|
|
16
|
+
|
|
17
|
+
<figure class="so-spark" data-so-spark="cable-rejections">
|
|
18
|
+
<figcaption class="so-spark__label">Rejections total <span data-so-range-copy><%= cable_range_label(@range) %></span></figcaption>
|
|
19
|
+
<svg class="so-spark__svg" viewBox="0 0 120 32" preserveAspectRatio="none" aria-hidden="true">
|
|
20
|
+
<line class="so-spark__baseline" x1="1" x2="119" y1="31" y2="31"/>
|
|
21
|
+
<polyline class="so-spark__line" points="<%= spark_points(@stats[:activity_trends][:rejections]) %>"/>
|
|
22
|
+
</svg>
|
|
23
|
+
<span class="so-spark__value"><%= number_with_delimiter(@stats[:rejections_count].to_i) %></span>
|
|
24
|
+
</figure>
|
|
25
|
+
<% else %>
|
|
26
|
+
<div class="so-card so-cable-dashboard__chart-empty">
|
|
27
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--empty-range">No chart data in the selected range yet. Summary metrics still use bounded cable stats.</p>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
30
|
+
</div>
|
|
31
|
+
</section>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<section class="so-card so-card--section" aria-labelledby="so-cable-events-heading">
|
|
2
|
+
<header class="so-dashboard-section__header">
|
|
3
|
+
<h2 id="so-cable-events-heading" class="so-dashboard-section__title">Recent Cable events</h2>
|
|
4
|
+
<span class="so-dashboard-section__meta">debug context only · broadcasting names and payloads are never shown</span>
|
|
5
|
+
</header>
|
|
6
|
+
|
|
7
|
+
<% if recent_events.present? %>
|
|
8
|
+
<table class="so-table so-table--card">
|
|
9
|
+
<caption class="sr-only">Recent Cable events without raw broadcasting names or payloads</caption>
|
|
10
|
+
<thead>
|
|
11
|
+
<tr>
|
|
12
|
+
<th>Event</th>
|
|
13
|
+
<th>Channel class</th>
|
|
14
|
+
<th>Broadcasting digest</th>
|
|
15
|
+
<th>Duration</th>
|
|
16
|
+
<th>Recorded</th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<% recent_events.each do |event| %>
|
|
21
|
+
<tr>
|
|
22
|
+
<td>
|
|
23
|
+
<%= event.event_type.to_s %>
|
|
24
|
+
<% if event.correlation_id.present? %>
|
|
25
|
+
<%= link_to "Trace →", trace_path(event.correlation_id), class: "so-trace__link" %>
|
|
26
|
+
<% end %>
|
|
27
|
+
</td>
|
|
28
|
+
<td><%= event.channel_class.presence || "—" %></td>
|
|
29
|
+
<td><span class="so-cable-dashboard__digest"><%= cable_event_digest(event.broadcasting_digest) %></span></td>
|
|
30
|
+
<td><%= event.duration ? format_duration(event.duration) : "—" %></td>
|
|
31
|
+
<td><span title="<%= event.recorded_at.utc.strftime("%Y-%m-%d %H:%M:%S UTC") %>"><%= time_ago_in_words(event.recorded_at) %> ago</span></td>
|
|
32
|
+
</tr>
|
|
33
|
+
<% end %>
|
|
34
|
+
</tbody>
|
|
35
|
+
</table>
|
|
36
|
+
<% else %>
|
|
37
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--empty-events">No sampled cable events in the selected range yet. Broadcasts, rejections, and errors will appear here after Cable activity is recorded.</p>
|
|
38
|
+
<% end %>
|
|
39
|
+
</section>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<% backlog_summary = cable_backlog_summary(stats) %>
|
|
2
|
+
<% storage_summary = cable_storage_summary(storage_components) %>
|
|
3
|
+
|
|
4
|
+
<section class="so-dashboard-section" aria-labelledby="so-cable-summary-heading">
|
|
5
|
+
<header class="so-dashboard-section__header">
|
|
6
|
+
<h2 id="so-cable-summary-heading" class="so-dashboard-section__title">Summary in selected range</h2>
|
|
7
|
+
</header>
|
|
8
|
+
|
|
9
|
+
<div class="so-stat-cards so-cable-dashboard__summary">
|
|
10
|
+
<article class="so-card so-metric">
|
|
11
|
+
<div class="so-card__label">Broadcasts</div>
|
|
12
|
+
<div class="so-metric__value"><%= number_with_delimiter(stats[:broadcasts_count].to_i) %></div>
|
|
13
|
+
<div class="so-card__subtitle">selected window</div>
|
|
14
|
+
</article>
|
|
15
|
+
|
|
16
|
+
<article class="so-card so-metric">
|
|
17
|
+
<div class="so-card__label">Rejection rate</div>
|
|
18
|
+
<div class="so-metric__value"><%= cable_ratio_percent(stats[:rejection_rate]) %></div>
|
|
19
|
+
<div class="so-card__subtitle">rejections / broadcasts</div>
|
|
20
|
+
</article>
|
|
21
|
+
|
|
22
|
+
<article class="so-card so-metric">
|
|
23
|
+
<div class="so-card__label">Message backlog</div>
|
|
24
|
+
<div class="so-metric__value"><%= backlog_summary[:value] %></div>
|
|
25
|
+
<div class="so-card__subtitle"><%= backlog_summary[:subtitle] %></div>
|
|
26
|
+
</article>
|
|
27
|
+
|
|
28
|
+
<article class="so-card so-metric">
|
|
29
|
+
<div class="so-card__label">Storage footprint</div>
|
|
30
|
+
<div class="so-metric__value"><%= storage_summary[:value] %></div>
|
|
31
|
+
<div class="so-card__subtitle"><%= storage_summary[:subtitle] %></div>
|
|
32
|
+
</article>
|
|
33
|
+
</div>
|
|
34
|
+
</section>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<% content_for :title, "Cable overview" %>
|
|
2
|
+
<% status_class = @cable_dashboard_available ? "so-badge--success" : "so-badge--warning" %>
|
|
3
|
+
|
|
4
|
+
<div class="so-dashboard so-cable-dashboard">
|
|
5
|
+
<span class="sr-only">Cable Dashboard</span>
|
|
6
|
+
|
|
7
|
+
<div class="so-content__header">
|
|
8
|
+
<h1>Cable overview</h1>
|
|
9
|
+
|
|
10
|
+
<% if @cable_dashboard_available %>
|
|
11
|
+
<div class="so-cable-dashboard__intro">
|
|
12
|
+
<span class="so-badge so-badge--pill <%= status_class %>">
|
|
13
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true">
|
|
14
|
+
<circle r="3" cx="3" cy="3" />
|
|
15
|
+
</svg>
|
|
16
|
+
Available
|
|
17
|
+
</span>
|
|
18
|
+
<p class="so-cable-dashboard__hint">Selected range: <%= cable_range_label(@range) %> · broadcasting names and payloads are never shown.</p>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<% if @cable_dashboard_available %>
|
|
24
|
+
<form action="<%= request.path %>" method="get" class="so-dashboard-toolbar so-cable-dashboard__range-form">
|
|
25
|
+
<label for="so-cable-range" class="so-card__label so-filters__label">Range</label>
|
|
26
|
+
<select id="so-cable-range" name="range" onchange="this.form.submit()">
|
|
27
|
+
<% SolidObserver::Services::CableStats::RANGES.each_key do |key| %>
|
|
28
|
+
<option value="<%= key %>" <%= "selected" if key == @range %>><%= cable_range_label(key) %></option>
|
|
29
|
+
<% end %>
|
|
30
|
+
</select>
|
|
31
|
+
<button type="submit" class="so-btn so-btn--refresh">Refresh data</button>
|
|
32
|
+
</form>
|
|
33
|
+
|
|
34
|
+
<%= render "solid_observer/cable_dashboard/summary", stats: @stats, storage_components: @storage_components %>
|
|
35
|
+
|
|
36
|
+
<section class="so-card so-card--section" aria-labelledby="so-cable-controls-heading">
|
|
37
|
+
<header class="so-dashboard-section__header">
|
|
38
|
+
<h2 id="so-cable-controls-heading" class="so-dashboard-section__title">Operational controls</h2>
|
|
39
|
+
</header>
|
|
40
|
+
|
|
41
|
+
<% cable_controls_available = SolidObserver::Services::CableOperations.available? && @stats&.[](:backlog_available) %>
|
|
42
|
+
<% cable_backlog_count = @stats&.[](:backlog_count).to_i %>
|
|
43
|
+
|
|
44
|
+
<% if cable_controls_available %>
|
|
45
|
+
<% if cable_backlog_count > 1000 %>
|
|
46
|
+
<div class="so-cache-control-row">
|
|
47
|
+
<div class="so-cache-control-row__copy">
|
|
48
|
+
<h3 class="so-cache-control-row__title">Use the Rake task</h3>
|
|
49
|
+
<p class="so-cache-control-row__body">More than 1,000 expired/trimmable Solid Cable messages are pending. The UI will not run this trim. Run solid_observer:cable:trim from the server instead.</p>
|
|
50
|
+
<span class="so-badge so-badge--pill so-badge--warning">
|
|
51
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true">
|
|
52
|
+
<circle r="3" cx="3" cy="3" />
|
|
53
|
+
</svg>
|
|
54
|
+
UI trim unavailable
|
|
55
|
+
</span>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
<% else %>
|
|
59
|
+
<div class="so-cache-control-row">
|
|
60
|
+
<div class="so-cache-control-row__copy">
|
|
61
|
+
<h3 class="so-cache-control-row__title">Trim expired messages</h3>
|
|
62
|
+
<p class="so-cache-control-row__body">Removes expired/trimmable Solid Cable messages only. Active messages remain available to Cable.</p>
|
|
63
|
+
<span class="so-badge so-badge--pill so-badge--success">
|
|
64
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true">
|
|
65
|
+
<circle r="3" cx="3" cy="3" />
|
|
66
|
+
</svg>
|
|
67
|
+
Trimmable backlog: <%= cable_backlog_count %>
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="so-cache-control-row__action">
|
|
72
|
+
<%= button_to "Trim expired messages",
|
|
73
|
+
trim_cable_operations_path,
|
|
74
|
+
method: :post,
|
|
75
|
+
class: "so-btn",
|
|
76
|
+
form: {class: "so-form--inline"} %>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<% end %>
|
|
80
|
+
<% else %>
|
|
81
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--idle">Cable controls are unavailable because Solid Cable support is disabled or not detected. No trim was attempted.</p>
|
|
82
|
+
<span class="so-badge so-badge--pill so-badge--warning">
|
|
83
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true">
|
|
84
|
+
<circle r="3" cx="3" cy="3" />
|
|
85
|
+
</svg>
|
|
86
|
+
Unavailable
|
|
87
|
+
</span>
|
|
88
|
+
<% end %>
|
|
89
|
+
</section>
|
|
90
|
+
|
|
91
|
+
<%= render "solid_observer/cable_dashboard/charts" %>
|
|
92
|
+
|
|
93
|
+
<% if @stats&.[](:stability)&.[](:available) %>
|
|
94
|
+
<section class="so-stability" data-so-zone="cable-stability" aria-labelledby="so-cable-stability-heading">
|
|
95
|
+
<span class="so-stability__label" id="so-cable-stability-heading">Stability</span>
|
|
96
|
+
<%= cable_stability_badge(@stats[:stability][:state]) %>
|
|
97
|
+
<span class="so-stability__detail"><%= cable_stability_detail(@stats[:stability]) %></span>
|
|
98
|
+
<a href="#so-cable-events-heading" class="so-stability__link">View recent events →</a>
|
|
99
|
+
</section>
|
|
100
|
+
<% end %>
|
|
101
|
+
|
|
102
|
+
<%= render "solid_observer/cable_dashboard/recent_events", recent_events: @recent_events %>
|
|
103
|
+
<% else %>
|
|
104
|
+
<section class="so-card so-card--section" aria-labelledby="so-cable-unavailable-heading">
|
|
105
|
+
<header class="so-dashboard-section__header">
|
|
106
|
+
<h2 id="so-cable-unavailable-heading" class="so-dashboard-section__title">Cable dashboard unavailable</h2>
|
|
107
|
+
<span class="so-badge so-badge--pill <%= status_class %>">
|
|
108
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true">
|
|
109
|
+
<circle r="3" cx="3" cy="3" />
|
|
110
|
+
</svg>
|
|
111
|
+
Unavailable
|
|
112
|
+
</span>
|
|
113
|
+
</header>
|
|
114
|
+
|
|
115
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--idle">Cable dashboard is unavailable because Solid Cable support is disabled or not detected. Metrics are unavailable.</p>
|
|
116
|
+
</section>
|
|
117
|
+
<% end %>
|
|
118
|
+
</div>
|
|
@@ -19,7 +19,12 @@
|
|
|
19
19
|
<tbody>
|
|
20
20
|
<% recent_events.each do |event| %>
|
|
21
21
|
<tr>
|
|
22
|
-
<td
|
|
22
|
+
<td>
|
|
23
|
+
<%= event.event_type.to_s.humanize %>
|
|
24
|
+
<% if event.correlation_id.present? %>
|
|
25
|
+
<%= link_to "Trace →", trace_path(event.correlation_id), class: "so-trace__link" %>
|
|
26
|
+
<% end %>
|
|
27
|
+
</td>
|
|
23
28
|
<td><span class="so-cache-dashboard__digest"><%= cache_event_digest(event.key_digest) %></span></td>
|
|
24
29
|
<td><%= cache_event_outcome_badge(event) %></td>
|
|
25
30
|
<td><%= event.duration ? format_duration(event.duration) : "—" %></td>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<% if health[:components].present? %>
|
|
2
|
+
<section class="so-cards-section" data-so-zone="home-cards" aria-labelledby="so-home-components-heading">
|
|
3
|
+
<h2 class="so-cards-section__title" id="so-home-components-heading">Components</h2>
|
|
4
|
+
<div class="so-stat-cards">
|
|
5
|
+
<% health[:components].each do |name, info| %>
|
|
6
|
+
<div class="so-card" aria-labelledby="so-card-<%= name %>">
|
|
7
|
+
<div class="so-card__label" id="so-card-<%= name %>"><%= name.to_s.titleize %></div>
|
|
8
|
+
<div class="so-card__value">
|
|
9
|
+
<%= health_status_badge(info[:status]) %>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="so-card__subtitle">
|
|
12
|
+
<% unless info[:available] %><span class="so-badge so-badge--default">Unavailable</span> · <% end %><%= link_to "View overview →", component_overview_path(name), class: "so-stability__link" %>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
</section>
|
|
18
|
+
<% end %>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<% non_stable = health[:components].count { |_n, i| i[:status].to_sym != :stable } %>
|
|
2
|
+
<section class="so-stability" data-so-zone="home-health" aria-labelledby="so-health-heading">
|
|
3
|
+
<span class="so-stability__label" id="so-health-heading">Overall Health</span>
|
|
4
|
+
<%= health_status_badge(health[:overall]) %>
|
|
5
|
+
<span class="so-stability__detail"><%= non_stable.zero? ? "All included components stable" : "#{non_stable} component#{'s' unless non_stable == 1} not stable" %></span>
|
|
6
|
+
</section>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<% content_for :title, "Home" %>
|
|
2
|
+
<div class="so-dashboard" data-so-zone="home">
|
|
3
|
+
<div class="so-content__header">
|
|
4
|
+
<h1>Home</h1>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<%= render "health", health: health %>
|
|
8
|
+
<%= render "component_cards", health: health %>
|
|
9
|
+
<%= render "unified_feed", feed: feed %>
|
|
10
|
+
</div>
|