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
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<div class="so-dashboard">
|
|
2
|
+
<% queue_enabled = SolidObserver.config.solid_queue_enabled? %>
|
|
3
|
+
<% queue_status_class = queue_enabled ? "so-badge--success" : "so-badge--warning" %>
|
|
4
|
+
|
|
5
|
+
<div class="so-content__header">
|
|
6
|
+
<h1>Queue overview</h1>
|
|
7
|
+
<div class="so-queue-overview__intro">
|
|
8
|
+
<span class="so-badge so-badge--pill <%= queue_status_class %>">
|
|
9
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true">
|
|
10
|
+
<circle r="3" cx="3" cy="3" />
|
|
11
|
+
</svg>
|
|
12
|
+
<%= queue_enabled ? "Available" : "Unavailable" %>
|
|
13
|
+
</span>
|
|
14
|
+
<% if queue_enabled %>
|
|
15
|
+
<p class="so-queue-overview__hint">Selected range: <%= range_label(@range) %></p>
|
|
16
|
+
<% end %>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<% if !queue_enabled %>
|
|
21
|
+
<section class="so-card so-card--section" aria-labelledby="so-queue-unavailable-heading">
|
|
22
|
+
<h2 id="so-queue-unavailable-heading">Queue Unavailable</h2>
|
|
23
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--idle">Queue component is disabled or not installed. Enable SolidQueue to access dashboard controls.</p>
|
|
24
|
+
</section>
|
|
25
|
+
<% else %>
|
|
26
|
+
<%# Zone A: Toolbar — range selector, Refresh, Live toggle, help disclosure %>
|
|
27
|
+
<div class="so-dashboard-toolbar" data-so-live data-so-poll-url="<%= poll_data_path %>">
|
|
28
|
+
<form action="<%= request.path %>" method="get" class="so-dashboard-toolbar__left">
|
|
29
|
+
<label for="so-range" class="so-card__label so-filters__label">Range</label>
|
|
30
|
+
<select id="so-range" name="range" data-so-range-select onchange="this.form.submit()">
|
|
31
|
+
<% SolidObserver::QueueStats::RANGES.each_key do |key| %>
|
|
32
|
+
<option value="<%= key %>" <%= "selected" if key == @range %>><%= key %></option>
|
|
33
|
+
<% end %>
|
|
34
|
+
</select>
|
|
35
|
+
<button type="button" class="so-btn so-btn--refresh" data-so-refresh aria-busy="false">Refresh data</button>
|
|
36
|
+
<span class="so-toolbar-freshness" data-so-freshness aria-live="polite"></span>
|
|
37
|
+
<input type="hidden" name="live" value="<%= @live ? 'on' : '' %>">
|
|
38
|
+
</form>
|
|
39
|
+
<div class="so-dashboard-toolbar__right">
|
|
40
|
+
<label class="so-toggle so-toggle--pill <%= "so-toggle--on" if @live %>">
|
|
41
|
+
<input type="checkbox" name="live" value="on" <%= "checked" if @live %> data-so-live-toggle>
|
|
42
|
+
<span class="so-toggle__track" aria-hidden="true">
|
|
43
|
+
<span class="so-toggle__thumb"></span>
|
|
44
|
+
</span>
|
|
45
|
+
<span class="so-toggle__label">Live</span>
|
|
46
|
+
<span class="so-toggle__sep" aria-hidden="true"> · </span>
|
|
47
|
+
<span class="so-toggle__cadence" aria-live="polite"><%= @live ? "5s" : "off" %></span>
|
|
48
|
+
<span class="so-toggle__dot" aria-hidden="true"></span>
|
|
49
|
+
</label>
|
|
50
|
+
<div class="so-help-wrapper" data-so-help-wrapper>
|
|
51
|
+
<button type="button" class="so-btn so-btn--help" data-so-help-btn aria-expanded="false" aria-controls="so-help-panel">(?)
|
|
52
|
+
</button>
|
|
53
|
+
<div id="so-help-panel" class="so-help-panel" data-so-help-panel hidden>
|
|
54
|
+
<p>Live mode polls every 5 seconds for current queue state. Throughput and chart data update only on range change or manual refresh.</p>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<%# Zone B: Right now — live state %>
|
|
61
|
+
<section class="so-dashboard-section" aria-labelledby="so-live-state-heading">
|
|
62
|
+
<div class="so-dashboard-section__header">
|
|
63
|
+
<h2 class="so-dashboard-section__title" id="so-live-state-heading">Right now</h2>
|
|
64
|
+
<span class="so-dashboard-section__meta"><span class="so-badge so-badge--pill so-badge--success"><svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true"><circle r="3" cx="3" cy="3" fill="var(--so-live-marker, var(--so-success))"/></svg></span> current</span>
|
|
65
|
+
</div>
|
|
66
|
+
<%= render "live_state", stats: @stats %>
|
|
67
|
+
</section>
|
|
68
|
+
|
|
69
|
+
<%# Zone C: Throughput in selected range %>
|
|
70
|
+
<% if persistence_mode? && @stats[:available] %>
|
|
71
|
+
<section class="so-dashboard-section" aria-labelledby="so-throughput-heading">
|
|
72
|
+
<div class="so-dashboard-section__header">
|
|
73
|
+
<h2 class="so-dashboard-section__title" id="so-throughput-heading">Throughput in selected range</h2>
|
|
74
|
+
<span class="so-dashboard-section__meta"><span class="so-badge so-badge--pill so-badge--info"><svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true"><circle r="3" cx="3" cy="3" fill="var(--so-range-marker, var(--so-info))"/></svg></span> <span data-so-range-copy><%= range_label(@range) %></span></span>
|
|
75
|
+
</div>
|
|
76
|
+
<%= render "throughput", stats: @stats, range: @range %>
|
|
77
|
+
</section>
|
|
78
|
+
<% end %>
|
|
79
|
+
|
|
80
|
+
<%# Zone D: Charts %>
|
|
81
|
+
<% if @stats[:available] %>
|
|
82
|
+
<section class="so-dashboard-section" aria-labelledby="so-chart-heading">
|
|
83
|
+
<div class="so-dashboard-section__header">
|
|
84
|
+
<h2 class="so-dashboard-section__title" id="so-chart-heading">Charts</h2>
|
|
85
|
+
</div>
|
|
86
|
+
<%= render "chart", range: @range %>
|
|
87
|
+
</section>
|
|
88
|
+
<% end %>
|
|
89
|
+
|
|
90
|
+
<%# Zone E: Per-queue table %>
|
|
91
|
+
<%= render "queue_table", stats: @stats, range: @range %>
|
|
92
|
+
|
|
93
|
+
<%# Zone F: Stability strip %>
|
|
94
|
+
<% if @stats[:available] && persistence_mode? %>
|
|
95
|
+
<section class="so-stability" data-so-zone="stability" aria-labelledby="so-stability-heading">
|
|
96
|
+
<span class="so-stability__label" id="so-stability-heading">Stability</span>
|
|
97
|
+
<%= stability_badge(@stats) %>
|
|
98
|
+
<span class="so-stability__detail"><%= stability_detail(@stats) %></span>
|
|
99
|
+
<%= link_to "View failures →", events_path(event_type: "job_failed"), class: "so-stability__link" %>
|
|
100
|
+
</section>
|
|
101
|
+
<% end %>
|
|
102
|
+
|
|
103
|
+
<%# Zone G: Recent Events %>
|
|
104
|
+
<% if @stats[:available] && persistence_mode? %>
|
|
105
|
+
<% if defined?(@recent_events) && @recent_events.present? %>
|
|
106
|
+
<div class="so-card">
|
|
107
|
+
<div class="so-card__label">Recent Events</div>
|
|
108
|
+
<table class="so-table so-table--card">
|
|
109
|
+
<caption class="sr-only">Recent queue events</caption>
|
|
110
|
+
<thead>
|
|
111
|
+
<tr>
|
|
112
|
+
<th>Event</th>
|
|
113
|
+
<th>Job Class</th>
|
|
114
|
+
<th>Queue</th>
|
|
115
|
+
<th>Time</th>
|
|
116
|
+
</tr>
|
|
117
|
+
</thead>
|
|
118
|
+
<tbody>
|
|
119
|
+
<% @recent_events.each do |event| %>
|
|
120
|
+
<tr>
|
|
121
|
+
<td><%= event.event_type.humanize %></td>
|
|
122
|
+
<td><%= event.job_class %></td>
|
|
123
|
+
<td><%= event.queue_name %></td>
|
|
124
|
+
<td><%= time_ago_in_words(event.recorded_at) %> ago</td>
|
|
125
|
+
</tr>
|
|
126
|
+
<% end %>
|
|
127
|
+
</tbody>
|
|
128
|
+
</table>
|
|
129
|
+
</div>
|
|
130
|
+
<% else %>
|
|
131
|
+
<div class="so-card">
|
|
132
|
+
<div class="so-card__label">Recent Events</div>
|
|
133
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--empty-events">No recent events yet. Queue activity will appear here after jobs are enqueued, performed, or failed.</p>
|
|
134
|
+
</div>
|
|
135
|
+
<% end %>
|
|
136
|
+
<% end %>
|
|
137
|
+
<% end %>
|
|
138
|
+
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<section class="so-card" data-so-zone="home-feed" aria-labelledby="so-feed-heading">
|
|
2
|
+
<div class="so-card__label" id="so-feed-heading">Recent Activity</div>
|
|
3
|
+
<% if SolidObserver.config.realtime_mode? %>
|
|
4
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--empty-events">Feed requires persistence mode. Switch to persistence mode to see recent activity across components.</p>
|
|
5
|
+
<% elsif feed.present? %>
|
|
6
|
+
<table class="so-table so-table--card">
|
|
7
|
+
<caption class="sr-only">Unified recent activity across enabled components</caption>
|
|
8
|
+
<thead>
|
|
9
|
+
<tr>
|
|
10
|
+
<th>Component</th>
|
|
11
|
+
<th>Event</th>
|
|
12
|
+
<th>Detail</th>
|
|
13
|
+
<th>Time</th>
|
|
14
|
+
</tr>
|
|
15
|
+
</thead>
|
|
16
|
+
<tbody>
|
|
17
|
+
<% feed.each do |row| %>
|
|
18
|
+
<tr>
|
|
19
|
+
<td><%= row[:component].to_s.titleize %></td>
|
|
20
|
+
<td><%= row[:event_type].to_s.humanize %></td>
|
|
21
|
+
<td>
|
|
22
|
+
<%= unified_feed_detail(row) %>
|
|
23
|
+
</td>
|
|
24
|
+
<td>
|
|
25
|
+
<% if row[:recorded_at] %>
|
|
26
|
+
<time datetime="<%= row[:recorded_at].iso8601 %>" title="<%= row[:recorded_at].iso8601 %>"><%= time_ago_in_words(row[:recorded_at]) %> ago</time>
|
|
27
|
+
<% else %>
|
|
28
|
+
—
|
|
29
|
+
<% end %>
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
<% end %>
|
|
33
|
+
</tbody>
|
|
34
|
+
</table>
|
|
35
|
+
<% else %>
|
|
36
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--empty-events">No recent activity yet. Events will appear here after components record activity.</p>
|
|
37
|
+
<% end %>
|
|
38
|
+
</section>
|
|
@@ -1,143 +1,7 @@
|
|
|
1
1
|
<% content_for :title, "Dashboard" %>
|
|
2
2
|
|
|
3
|
-
<% if @component == "
|
|
4
|
-
<%= render
|
|
3
|
+
<% if @component == "home" %>
|
|
4
|
+
<%= render "home", health: @health, feed: @feed %>
|
|
5
5
|
<% else %>
|
|
6
|
-
|
|
7
|
-
<% queue_enabled = SolidObserver.config.solid_queue_enabled? %>
|
|
8
|
-
<% queue_status_class = queue_enabled ? "so-badge--success" : "so-badge--warning" %>
|
|
9
|
-
|
|
10
|
-
<div class="so-content__header">
|
|
11
|
-
<h1>Queue overview</h1>
|
|
12
|
-
<div class="so-queue-overview__intro">
|
|
13
|
-
<span class="so-badge so-badge--pill <%= queue_status_class %>">
|
|
14
|
-
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true">
|
|
15
|
-
<circle r="3" cx="3" cy="3" />
|
|
16
|
-
</svg>
|
|
17
|
-
<%= queue_enabled ? "Available" : "Unavailable" %>
|
|
18
|
-
</span>
|
|
19
|
-
<% if queue_enabled %>
|
|
20
|
-
<p class="so-queue-overview__hint">Selected range: <%= range_label(@range) %></p>
|
|
21
|
-
<% end %>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
|
|
25
|
-
<% if !queue_enabled %>
|
|
26
|
-
<section class="so-card so-card--section" aria-labelledby="so-queue-unavailable-heading">
|
|
27
|
-
<h2 id="so-queue-unavailable-heading">Queue Unavailable</h2>
|
|
28
|
-
<p class="so-dashboard-section__meta so-dashboard-section__meta--idle">Queue component is disabled or not installed. Enable SolidQueue to access dashboard controls.</p>
|
|
29
|
-
</section>
|
|
30
|
-
<% else %>
|
|
31
|
-
<%# Zone A: Toolbar — range selector, Refresh, Live toggle, help disclosure %>
|
|
32
|
-
<div class="so-dashboard-toolbar" data-so-live>
|
|
33
|
-
<form action="<%= request.path %>" method="get" class="so-dashboard-toolbar__left">
|
|
34
|
-
<label for="so-range" class="so-card__label so-filters__label">Range</label>
|
|
35
|
-
<select id="so-range" name="range" data-so-range-select onchange="this.form.submit()">
|
|
36
|
-
<% SolidObserver::QueueStats::RANGES.each_key do |key| %>
|
|
37
|
-
<option value="<%= key %>" <%= "selected" if key == @range %>><%= key %></option>
|
|
38
|
-
<% end %>
|
|
39
|
-
</select>
|
|
40
|
-
<button type="button" class="so-btn so-btn--refresh" data-so-refresh aria-busy="false">Refresh data</button>
|
|
41
|
-
<span class="so-toolbar-freshness" data-so-freshness aria-live="polite"></span>
|
|
42
|
-
<input type="hidden" name="live" value="<%= @live ? 'on' : '' %>">
|
|
43
|
-
</form>
|
|
44
|
-
<div class="so-dashboard-toolbar__right">
|
|
45
|
-
<label class="so-toggle so-toggle--pill <%= "so-toggle--on" if @live %>">
|
|
46
|
-
<input type="checkbox" name="live" value="on" <%= "checked" if @live %> data-so-live-toggle>
|
|
47
|
-
<span class="so-toggle__track" aria-hidden="true">
|
|
48
|
-
<span class="so-toggle__thumb"></span>
|
|
49
|
-
</span>
|
|
50
|
-
<span class="so-toggle__label">Live</span>
|
|
51
|
-
<span class="so-toggle__sep" aria-hidden="true"> · </span>
|
|
52
|
-
<span class="so-toggle__cadence" aria-live="polite"><%= @live ? "5s" : "off" %></span>
|
|
53
|
-
<span class="so-toggle__dot" aria-hidden="true"></span>
|
|
54
|
-
</label>
|
|
55
|
-
<div class="so-help-wrapper" data-so-help-wrapper>
|
|
56
|
-
<button type="button" class="so-btn so-btn--help" data-so-help-btn aria-expanded="false" aria-controls="so-help-panel">(?)
|
|
57
|
-
</button>
|
|
58
|
-
<div id="so-help-panel" class="so-help-panel" data-so-help-panel hidden>
|
|
59
|
-
<p>Live mode polls every 5 seconds for current queue state. Throughput and chart data update only on range change or manual refresh.</p>
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
<%# Zone B: Right now — live state %>
|
|
66
|
-
<section class="so-dashboard-section" aria-labelledby="so-live-state-heading">
|
|
67
|
-
<div class="so-dashboard-section__header">
|
|
68
|
-
<h2 class="so-dashboard-section__title" id="so-live-state-heading">Right now</h2>
|
|
69
|
-
<span class="so-dashboard-section__meta"><span class="so-badge so-badge--pill so-badge--success"><svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true"><circle r="3" cx="3" cy="3" fill="var(--so-live-marker, var(--so-success))"/></svg></span> current</span>
|
|
70
|
-
</div>
|
|
71
|
-
<%= render "live_state", stats: @stats %>
|
|
72
|
-
</section>
|
|
73
|
-
|
|
74
|
-
<%# Zone C: Throughput in selected range %>
|
|
75
|
-
<% if persistence_mode? && @stats[:available] %>
|
|
76
|
-
<section class="so-dashboard-section" aria-labelledby="so-throughput-heading">
|
|
77
|
-
<div class="so-dashboard-section__header">
|
|
78
|
-
<h2 class="so-dashboard-section__title" id="so-throughput-heading">Throughput in selected range</h2>
|
|
79
|
-
<span class="so-dashboard-section__meta"><span class="so-badge so-badge--pill so-badge--info"><svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true"><circle r="3" cx="3" cy="3" fill="var(--so-range-marker, var(--so-info))"/></svg></span> <span data-so-range-copy><%= range_label(@range) %></span></span>
|
|
80
|
-
</div>
|
|
81
|
-
<%= render "throughput", stats: @stats, range: @range %>
|
|
82
|
-
</section>
|
|
83
|
-
<% end %>
|
|
84
|
-
|
|
85
|
-
<%# Zone D: Charts %>
|
|
86
|
-
<% if @stats[:available] %>
|
|
87
|
-
<section class="so-dashboard-section" aria-labelledby="so-chart-heading">
|
|
88
|
-
<div class="so-dashboard-section__header">
|
|
89
|
-
<h2 class="so-dashboard-section__title" id="so-chart-heading">Charts</h2>
|
|
90
|
-
</div>
|
|
91
|
-
<%= render "chart", range: @range %>
|
|
92
|
-
</section>
|
|
93
|
-
<% end %>
|
|
94
|
-
|
|
95
|
-
<%# Zone E: Per-queue table %>
|
|
96
|
-
<%= render "queue_table", stats: @stats, range: @range %>
|
|
97
|
-
|
|
98
|
-
<%# Zone F: Stability strip %>
|
|
99
|
-
<% if @stats[:available] && persistence_mode? %>
|
|
100
|
-
<section class="so-stability" data-so-zone="stability" aria-labelledby="so-stability-heading">
|
|
101
|
-
<span class="so-stability__label" id="so-stability-heading">Stability</span>
|
|
102
|
-
<%= stability_badge(@stats) %>
|
|
103
|
-
<span class="so-stability__detail"><%= stability_detail(@stats) %></span>
|
|
104
|
-
<%= link_to "View failures →", events_path(event_type: "job_failed"), class: "so-stability__link" %>
|
|
105
|
-
</section>
|
|
106
|
-
<% end %>
|
|
107
|
-
|
|
108
|
-
<%# Zone G: Recent Events %>
|
|
109
|
-
<% if @stats[:available] && persistence_mode? %>
|
|
110
|
-
<% if defined?(@recent_events) && @recent_events.present? %>
|
|
111
|
-
<div class="so-card">
|
|
112
|
-
<div class="so-card__label">Recent Events</div>
|
|
113
|
-
<table class="so-table so-table--card">
|
|
114
|
-
<thead>
|
|
115
|
-
<tr>
|
|
116
|
-
<th>Event</th>
|
|
117
|
-
<th>Job Class</th>
|
|
118
|
-
<th>Queue</th>
|
|
119
|
-
<th>Time</th>
|
|
120
|
-
</tr>
|
|
121
|
-
</thead>
|
|
122
|
-
<tbody>
|
|
123
|
-
<% @recent_events.each do |event| %>
|
|
124
|
-
<tr>
|
|
125
|
-
<td><%= event.event_type.humanize %></td>
|
|
126
|
-
<td><%= event.job_class %></td>
|
|
127
|
-
<td><%= event.queue_name %></td>
|
|
128
|
-
<td><%= time_ago_in_words(event.recorded_at) %> ago</td>
|
|
129
|
-
</tr>
|
|
130
|
-
<% end %>
|
|
131
|
-
</tbody>
|
|
132
|
-
</table>
|
|
133
|
-
</div>
|
|
134
|
-
<% else %>
|
|
135
|
-
<div class="so-card">
|
|
136
|
-
<div class="so-card__label">Recent Events</div>
|
|
137
|
-
<p class="so-dashboard-section__meta so-dashboard-section__meta--empty-events">No recent events yet. Queue activity will appear here after jobs are enqueued, performed, or failed.</p>
|
|
138
|
-
</div>
|
|
139
|
-
<% end %>
|
|
140
|
-
<% end %>
|
|
141
|
-
<% end %>
|
|
142
|
-
</div>
|
|
6
|
+
<%= render "queue" %>
|
|
143
7
|
<% end %>
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
<% if @events.any? %>
|
|
27
27
|
<table class="so-table so-table--listing">
|
|
28
|
+
<caption class="sr-only">Events list</caption>
|
|
28
29
|
<thead>
|
|
29
30
|
<tr>
|
|
30
31
|
<th>Event</th>
|
|
@@ -37,7 +38,12 @@
|
|
|
37
38
|
<tbody>
|
|
38
39
|
<% @events.each do |event| %>
|
|
39
40
|
<tr>
|
|
40
|
-
<td
|
|
41
|
+
<td>
|
|
42
|
+
<%= link_to event.event_type.humanize, event_path(event.id) %>
|
|
43
|
+
<% if event.correlation_id.present? %>
|
|
44
|
+
<%= link_to "Trace →", trace_path(event.correlation_id), class: "so-trace__link" %>
|
|
45
|
+
<% end %>
|
|
46
|
+
</td>
|
|
41
47
|
<td><%= event.job_class %></td>
|
|
42
48
|
<td><%= event.queue_name %></td>
|
|
43
49
|
<td><%= duration_with_semantic(event.duration, event.event_type) %></td>
|
|
@@ -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>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<% content_for :title, "Trace #{params[:id]}" %>
|
|
2
|
+
|
|
3
|
+
<div class="so-trace">
|
|
4
|
+
<div class="so-back-link">
|
|
5
|
+
<%= link_to "← Back to Events", events_path, class: "so-btn" %>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="so-content__header">
|
|
9
|
+
<h1>Trace <span class="so-trace__correlation"><%= params[:id] %></span></h1>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<% if @rows.any? %>
|
|
13
|
+
<p class="so-trace__meta">Showing newest <%= @rows.size %> events (maximum 100)</p>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<% if @unavailable_components.any? %>
|
|
17
|
+
<div class="so-flash so-flash--notice" role="status">
|
|
18
|
+
Some components could not be read: <%= @unavailable_components.map(&:to_s).sort.join(", ") %>. Showing available events only.
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
21
|
+
|
|
22
|
+
<% if @rows.any? %>
|
|
23
|
+
<% component_tones = {queue: "info", cache: "success", cable: "warning"} %>
|
|
24
|
+
<table class="so-table so-table--listing so-trace__table">
|
|
25
|
+
<caption class="sr-only">Trace timeline for correlation ID <%= params[:id] %></caption>
|
|
26
|
+
<thead>
|
|
27
|
+
<tr>
|
|
28
|
+
<th class="so-trace__time">Time</th>
|
|
29
|
+
<th class="so-trace__component">Component</th>
|
|
30
|
+
<th class="so-trace__event">Event</th>
|
|
31
|
+
<th class="so-trace__details">Details</th>
|
|
32
|
+
</tr>
|
|
33
|
+
</thead>
|
|
34
|
+
<tbody>
|
|
35
|
+
<% @rows.each do |row| %>
|
|
36
|
+
<tr>
|
|
37
|
+
<td class="so-trace__time"><%= row[:recorded_at].strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
38
|
+
<td class="so-trace__component">
|
|
39
|
+
<% component_tone = component_tones[row[:component]] || "default" %>
|
|
40
|
+
<span class="so-badge so-badge--pill so-badge--<%= component_tone %>">
|
|
41
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true"><circle r="3" cx="3" cy="3" /></svg>
|
|
42
|
+
<%= row[:component].to_s.capitalize %>
|
|
43
|
+
</span>
|
|
44
|
+
</td>
|
|
45
|
+
<td class="so-trace__event"><%= row[:event_type] %></td>
|
|
46
|
+
<td class="so-trace__details">
|
|
47
|
+
<% if row[:event_type] == "cable_summary" %>
|
|
48
|
+
<%= row[:collapsed_count] %> additional Cable broadcast events collapsed
|
|
49
|
+
<% else %>
|
|
50
|
+
<% case row[:component]
|
|
51
|
+
when :queue %>
|
|
52
|
+
<%= [
|
|
53
|
+
("job=#{row[:job_class]}" if row[:job_class]),
|
|
54
|
+
("queue=#{row[:queue_name]}" if row[:queue_name]),
|
|
55
|
+
("duration=#{format_duration(row[:duration])}" if row[:duration])
|
|
56
|
+
].compact.join(" ") %>
|
|
57
|
+
<% when :cache %>
|
|
58
|
+
<%= [
|
|
59
|
+
("hit=#{row[:hit]}" unless row[:hit].nil?),
|
|
60
|
+
("error=#{row[:error_class]}" if row[:error_class]),
|
|
61
|
+
("duration=#{format_duration(row[:duration])}" if row[:duration])
|
|
62
|
+
].compact.join(" ") %>
|
|
63
|
+
<% when :cable %>
|
|
64
|
+
<%= [
|
|
65
|
+
("channel=#{row[:channel_class]}" if row[:channel_class]),
|
|
66
|
+
("error=#{row[:error_class]}" if row[:error_class]),
|
|
67
|
+
("duration=#{format_duration(row[:duration])}" if row[:duration]),
|
|
68
|
+
("collapsed=#{row[:collapsed_count]}" if row[:collapsed_count])
|
|
69
|
+
].compact.join(" ") %>
|
|
70
|
+
<% end %>
|
|
71
|
+
<% end %>
|
|
72
|
+
</td>
|
|
73
|
+
</tr>
|
|
74
|
+
<% end %>
|
|
75
|
+
</tbody>
|
|
76
|
+
</table>
|
|
77
|
+
<% else %>
|
|
78
|
+
<%= render "solid_observer/shared/empty_state", message: "No events found for correlation ID #{params[:id]}. The trace may have expired, not yet recorded, or belong to an unavailable component." %>
|
|
79
|
+
<% end %>
|
|
80
|
+
</div>
|
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
|
|
@@ -19,4 +21,5 @@ SolidObserver::Engine.routes.draw do
|
|
|
19
21
|
|
|
20
22
|
resource :storage, only: %i[show]
|
|
21
23
|
resources :events, only: %i[index show]
|
|
24
|
+
resources :traces, only: [:show]
|
|
22
25
|
end
|
|
@@ -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
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddCorrelationIdToCacheEvents < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
add_column :solid_observer_cache_events, :correlation_id, :string, limit: 64
|
|
6
|
+
add_index :solid_observer_cache_events, :correlation_id, where: "correlation_id IS NOT NULL"
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddCorrelationIdToCableEvents < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
add_column :solid_observer_cable_events, :correlation_id, :string, limit: 64
|
|
6
|
+
add_index :solid_observer_cable_events, :correlation_id, where: "correlation_id IS NOT NULL"
|
|
7
|
+
end
|
|
8
|
+
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"
|