solid_observer 0.4.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 +13 -0
- data/README.md +80 -20
- 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 +1 -7
- data/app/helpers/solid_observer/application_helper.rb +114 -0
- 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 +19 -8
- 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/dashboard/_queue_table.html.erb +1 -0
- data/app/views/solid_observer/dashboard/index.html.erb +2 -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/storages/show.html.erb +29 -3
- data/config/routes.rb +2 -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 +18 -3
- 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/configuration.rb +47 -4
- data/lib/solid_observer/engine.rb +46 -28
- 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_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/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 +23 -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/version.rb +1 -1
- data/lib/solid_observer.rb +36 -11
- data/lib/tasks/solid_observer.rake +84 -23
- metadata +26 -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
|
@@ -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
|
}
|
|
@@ -564,7 +570,12 @@
|
|
|
564
570
|
<% end %>
|
|
565
571
|
<% end %>
|
|
566
572
|
|
|
567
|
-
<% if
|
|
573
|
+
<% if SolidObserver.config.solid_cable_enabled? %>
|
|
574
|
+
<div class="so-sidebar__section">Cable</div>
|
|
575
|
+
<%= link_to "Overview", cable_dashboard_path, class: ("active" if controller_name == "cable_dashboard"), "aria-current": (controller_name == "cable_dashboard" ? "page" : nil) %>
|
|
576
|
+
<% end %>
|
|
577
|
+
|
|
578
|
+
<% if persistence_mode? && (SolidObserver.config.solid_queue_enabled? || SolidObserver.config.solid_cache_enabled? || SolidObserver.config.solid_cable_enabled?) %>
|
|
568
579
|
<%= link_to "Storage", storage_path, class: ("active" if controller_name == "storages"), "aria-current": (controller_name == "storages" ? "page" : nil) %>
|
|
569
580
|
<% end %>
|
|
570
581
|
</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,34 @@
|
|
|
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><%= event.event_type.to_s %></td>
|
|
23
|
+
<td><%= event.channel_class.presence || "—" %></td>
|
|
24
|
+
<td><span class="so-cable-dashboard__digest"><%= cable_event_digest(event.broadcasting_digest) %></span></td>
|
|
25
|
+
<td><%= event.duration ? format_duration(event.duration) : "—" %></td>
|
|
26
|
+
<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>
|
|
27
|
+
</tr>
|
|
28
|
+
<% end %>
|
|
29
|
+
</tbody>
|
|
30
|
+
</table>
|
|
31
|
+
<% else %>
|
|
32
|
+
<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>
|
|
33
|
+
<% end %>
|
|
34
|
+
</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>
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
<% content_for :title, "Dashboard" %>
|
|
2
2
|
|
|
3
|
-
<% if @component == "cache" %>
|
|
4
|
-
<%= render template: "solid_observer/cache_dashboard/index" %>
|
|
5
|
-
<% else %>
|
|
6
3
|
<div class="so-dashboard">
|
|
7
4
|
<% queue_enabled = SolidObserver.config.solid_queue_enabled? %>
|
|
8
5
|
<% queue_status_class = queue_enabled ? "so-badge--success" : "so-badge--warning" %>
|
|
@@ -29,7 +26,7 @@
|
|
|
29
26
|
</section>
|
|
30
27
|
<% else %>
|
|
31
28
|
<%# Zone A: Toolbar — range selector, Refresh, Live toggle, help disclosure %>
|
|
32
|
-
<div class="so-dashboard-toolbar" data-so-live>
|
|
29
|
+
<div class="so-dashboard-toolbar" data-so-live data-so-poll-url="<%= poll_data_path %>">
|
|
33
30
|
<form action="<%= request.path %>" method="get" class="so-dashboard-toolbar__left">
|
|
34
31
|
<label for="so-range" class="so-card__label so-filters__label">Range</label>
|
|
35
32
|
<select id="so-range" name="range" data-so-range-select onchange="this.form.submit()">
|
|
@@ -111,6 +108,7 @@
|
|
|
111
108
|
<div class="so-card">
|
|
112
109
|
<div class="so-card__label">Recent Events</div>
|
|
113
110
|
<table class="so-table so-table--card">
|
|
111
|
+
<caption class="sr-only">Recent queue events</caption>
|
|
114
112
|
<thead>
|
|
115
113
|
<tr>
|
|
116
114
|
<th>Event</th>
|
|
@@ -140,4 +138,3 @@
|
|
|
140
138
|
<% end %>
|
|
141
139
|
<% end %>
|
|
142
140
|
</div>
|
|
143
|
-
<% end %>
|
|
@@ -28,7 +28,24 @@
|
|
|
28
28
|
<div class="so-card__value"><%= size_value %></div>
|
|
29
29
|
<div class="so-card__subtitle">
|
|
30
30
|
<% if component[:available] %>
|
|
31
|
-
|
|
31
|
+
<% if component[:component] == "solid_cable" %>
|
|
32
|
+
<% parts = ["#{records_value} #{component[:record_label]}"] %>
|
|
33
|
+
<% parts << "#{number_with_delimiter(component[:trimmable_count])} trimmable" unless component[:trimmable_count].nil? %>
|
|
34
|
+
<% unless component[:oldest_message_age_seconds].nil? %>
|
|
35
|
+
<% age = component[:oldest_message_age_seconds] %>
|
|
36
|
+
<% if age < 3600 %>
|
|
37
|
+
<% value = [age / 60, 1].max; unit = "m" %>
|
|
38
|
+
<% elsif age < 86_400 %>
|
|
39
|
+
<% value = age / 3600; unit = "h" %>
|
|
40
|
+
<% else %>
|
|
41
|
+
<% value = age / 86_400; unit = "d" %>
|
|
42
|
+
<% end %>
|
|
43
|
+
<% parts << "oldest #{value}#{unit}" %>
|
|
44
|
+
<% end %>
|
|
45
|
+
<%= parts.join(" · ") %>
|
|
46
|
+
<% else %>
|
|
47
|
+
<%= records_value %> <%= component[:record_label] %>
|
|
48
|
+
<% end %>
|
|
32
49
|
<% else %>
|
|
33
50
|
<%= component[:unavailable_reason] %>
|
|
34
51
|
<% end %>
|
|
@@ -53,10 +70,19 @@
|
|
|
53
70
|
</thead>
|
|
54
71
|
<tbody>
|
|
55
72
|
<% @storage_history.each do |snapshot| %>
|
|
56
|
-
<% record_label = snapshot.component
|
|
73
|
+
<% record_label = case snapshot.component
|
|
74
|
+
when "solid_cache" then "cache rows"
|
|
75
|
+
when "solid_cable" then "messages"
|
|
76
|
+
else "observer events"
|
|
77
|
+
end %>
|
|
78
|
+
<% component_label = case snapshot.component
|
|
79
|
+
when "solid_cable" then "Solid Cable messages"
|
|
80
|
+
when "cable_observer" then "Cable telemetry"
|
|
81
|
+
else snapshot.component.humanize
|
|
82
|
+
end %>
|
|
57
83
|
<tr>
|
|
58
84
|
<td><%= snapshot.recorded_at.strftime("%Y-%m-%d %H:%M") %></td>
|
|
59
|
-
<td><%=
|
|
85
|
+
<td><%= component_label %></td>
|
|
60
86
|
<td><%= number_to_human_size(snapshot.db_size_bytes, precision: 1, significant: false, strip_insignificant_zeros: false) %></td>
|
|
61
87
|
<td><%= number_with_delimiter(snapshot.event_count) %> <%= record_label %></td>
|
|
62
88
|
</tr>
|
data/config/routes.rb
CHANGED
|
@@ -4,6 +4,8 @@ SolidObserver::Engine.routes.draw do
|
|
|
4
4
|
root "dashboard#index"
|
|
5
5
|
get "queue", to: "dashboard#index", defaults: {component: "queue"}, as: :queue_dashboard
|
|
6
6
|
get "cache", to: "cache_dashboard#index", as: :cache_dashboard
|
|
7
|
+
get "cable", to: "cable_dashboard#index", as: :cable_dashboard
|
|
8
|
+
post "cable/trim", to: "cable_operations#trim", as: :trim_cable_operations
|
|
7
9
|
get "cache/controls", to: "cache_operations#index", as: :cache_operations
|
|
8
10
|
post "cache/controls/prune", to: "cache_operations#prune", as: :prune_cache_operations
|
|
9
11
|
post "cache/controls/clear", to: "cache_operations#clear", as: :clear_cache_operations
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddEventTypeRecordedAtIndexToCacheEvents < ActiveRecord::Migration[8.0]
|
|
4
|
+
disable_ddl_transaction!
|
|
5
|
+
|
|
6
|
+
def change
|
|
7
|
+
options = concurrent_supported? ? {algorithm: :concurrently} : {}
|
|
8
|
+
|
|
9
|
+
add_index :solid_observer_cache_events,
|
|
10
|
+
%i[event_type recorded_at],
|
|
11
|
+
order: {recorded_at: :desc},
|
|
12
|
+
if_not_exists: true,
|
|
13
|
+
**options
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def concurrent_supported?
|
|
19
|
+
connection.adapter_name.match?(/postgres/i)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateSolidObserverCableEvents < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :solid_observer_cable_events do |t|
|
|
6
|
+
t.string :event_type, null: false, limit: 64
|
|
7
|
+
t.string :channel_class, limit: 255
|
|
8
|
+
t.string :broadcasting_digest, limit: 64
|
|
9
|
+
t.float :duration
|
|
10
|
+
t.string :error_class, limit: 255
|
|
11
|
+
t.text :error_message
|
|
12
|
+
t.text :metadata
|
|
13
|
+
t.datetime :recorded_at, null: false
|
|
14
|
+
|
|
15
|
+
t.index :recorded_at
|
|
16
|
+
t.index :event_type
|
|
17
|
+
t.index :channel_class
|
|
18
|
+
t.index :broadcasting_digest
|
|
19
|
+
t.index :error_class
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateSolidObserverCableMetrics < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :solid_observer_cable_metrics do |t|
|
|
6
|
+
t.datetime :period_start, null: false
|
|
7
|
+
t.bigint :broadcasts_count, null: false, default: 0
|
|
8
|
+
t.bigint :transmissions_count, null: false, default: 0
|
|
9
|
+
t.bigint :confirmations_count, null: false, default: 0
|
|
10
|
+
t.bigint :rejections_count, null: false, default: 0
|
|
11
|
+
t.bigint :perform_actions_count, null: false, default: 0
|
|
12
|
+
t.bigint :errors_count, null: false, default: 0
|
|
13
|
+
|
|
14
|
+
t.index :period_start, unique: true, name: "idx_solid_observer_cable_metrics_unique"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -17,8 +17,11 @@ module SolidObserver
|
|
|
17
17
|
%w[development test production].each do |env|
|
|
18
18
|
config_block = <<-YAML
|
|
19
19
|
solid_observer_queue:
|
|
20
|
-
|
|
20
|
+
adapter: sqlite3
|
|
21
|
+
pool: 5
|
|
22
|
+
timeout: 5000
|
|
21
23
|
database: storage/#{env}_solid_observer_queue.sqlite3
|
|
24
|
+
migrations_paths: db/solid_observer_migrate
|
|
22
25
|
YAML
|
|
23
26
|
inject_into_file "config/database.yml", config_block, after: /^#{env}:\n(?: .*\n)*/
|
|
24
27
|
end
|
|
@@ -40,6 +43,10 @@ module SolidObserver
|
|
|
40
43
|
say " 5. Restart your Rails server"
|
|
41
44
|
say " 6. Visit /solid_observer to access the web dashboard"
|
|
42
45
|
say "\n"
|
|
46
|
+
say "IMPORTANT: If your host app uses PostgreSQL or MySQL, review the", :yellow
|
|
47
|
+
say "solid_observer_queue entries in config/database.yml before running", :yellow
|
|
48
|
+
say "db:create. The generated config uses adapter: sqlite3 by default.", :yellow
|
|
49
|
+
say "\n"
|
|
43
50
|
say "Documentation: https://solid.observer", :red
|
|
44
51
|
say "GitHub: https://github.com/bart-oz/solid_observer", :red
|
|
45
52
|
say "\n"
|
|
@@ -9,11 +9,20 @@ 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
|
-
#
|
|
12
|
+
# Production dashboard exposure is the host app's responsibility. If you mount
|
|
13
|
+
# the dashboard in production, wrap the mount in your existing admin auth
|
|
14
|
+
# constraint, for example in config/routes.rb:
|
|
15
|
+
#
|
|
16
|
+
# authenticate :user, ->(user) { user.admin? } do
|
|
17
|
+
# mount SolidObserver::Engine, at: "/solid_observer"
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# Built-in HTTP Basic Auth is enabled only when BOTH credentials are present.
|
|
21
|
+
# Setting only one credential disables Basic Auth and logs a boot warning.
|
|
13
22
|
# config.ui_username = "admin"
|
|
14
23
|
# config.ui_password = "secret"
|
|
15
24
|
|
|
16
|
-
# Base controller
|
|
25
|
+
# Base controller used only to detect API-only rendering compatibility
|
|
17
26
|
# config.ui_base_controller = "ApplicationController"
|
|
18
27
|
|
|
19
28
|
# === Queue Observability (v0.1.0) ===
|
|
@@ -24,8 +33,14 @@ SolidObserver.configure do |config|
|
|
|
24
33
|
# config.observe_cache = true
|
|
25
34
|
# config.cache_sampling_rate = 0.1 # Sample 10% of cache operations
|
|
26
35
|
|
|
27
|
-
# === Cable Observability (
|
|
36
|
+
# === Cable Observability (v0.5.0) ===
|
|
37
|
+
# Enable SolidCable event capture and trim controls.
|
|
38
|
+
# Requires SolidCable in the host app; SolidObserver does not add it.
|
|
28
39
|
# config.observe_cable = true
|
|
40
|
+
# config.cable_sampling_rate = 0.1 # Sample 10% of broadcast events
|
|
41
|
+
# config.cable_rejection_threshold = 0.05 # Rejection rate → Degraded stability
|
|
42
|
+
# config.cable_backlog_threshold = 0.10 # Backlog ratio threshold
|
|
43
|
+
# config.cable_error_threshold = 0.0 # Error rate threshold
|
|
29
44
|
|
|
30
45
|
# Data Retention
|
|
31
46
|
config.event_retention = 30.days # How long to keep event records
|
|
@@ -7,7 +7,7 @@ module SolidObserver
|
|
|
7
7
|
# will be configured by the Engine (similar to BaseEvent) when metrics are
|
|
8
8
|
# fully implemented.
|
|
9
9
|
#
|
|
10
|
-
class BaseMetric <
|
|
10
|
+
class BaseMetric < BaseRecord
|
|
11
11
|
self.abstract_class = true
|
|
12
12
|
self.table_name = "solid_observer_metrics"
|
|
13
13
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "singleton"
|
|
4
|
+
|
|
5
|
+
require_relative "event_buffer_core"
|
|
6
|
+
|
|
7
|
+
module SolidObserver
|
|
8
|
+
class CableEventBuffer
|
|
9
|
+
include Singleton
|
|
10
|
+
include EventBufferCore
|
|
11
|
+
|
|
12
|
+
INITIAL_METRICS = EventBufferCore::INITIAL_METRICS
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
initialize_event_buffer
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def flush_service
|
|
21
|
+
Services::FlushCableEventBuffer
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def log_label
|
|
25
|
+
"Cable buffer"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|