solid_observer 0.1.1 → 0.4.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 +84 -0
- data/README.md +241 -59
- data/app/assets/javascripts/solid_observer/live_poll.js +376 -0
- data/app/assets/stylesheets/solid_observer/application.css +18 -0
- data/app/controllers/concerns/solid_observer/paginatable.rb +17 -0
- data/app/controllers/concerns/solid_observer/require_persistence_mode.rb +19 -0
- data/app/controllers/concerns/solid_observer/require_solid_queue.rb +19 -0
- data/app/controllers/solid_observer/application_controller.rb +69 -0
- data/app/controllers/solid_observer/cache_dashboard_controller.rb +59 -0
- data/app/controllers/solid_observer/cache_operations_controller.rb +24 -0
- data/app/controllers/solid_observer/dashboard_controller.rb +122 -0
- data/app/controllers/solid_observer/events_controller.rb +50 -0
- data/app/controllers/solid_observer/jobs_controller.rb +85 -0
- data/app/controllers/solid_observer/storages_controller.rb +12 -0
- data/app/helpers/solid_observer/application_helper.rb +244 -0
- data/app/helpers/solid_observer/dashboard_helper.rb +58 -0
- data/app/models/solid_observer/cache_event.rb +15 -0
- data/app/models/solid_observer/cache_metric.rb +14 -0
- data/app/models/solid_observer/queue_event.rb +134 -0
- data/app/models/solid_observer/queue_metric.rb +1 -1
- data/app/models/solid_observer/storage_info.rb +4 -1
- data/app/presenters/solid_observer/execution_presenter.rb +50 -0
- data/app/views/layouts/solid_observer/application.html.erb +597 -0
- data/app/views/solid_observer/cache_dashboard/_charts.html.erb +40 -0
- data/app/views/solid_observer/cache_dashboard/_recent_events.html.erb +34 -0
- data/app/views/solid_observer/cache_dashboard/_summary.html.erb +39 -0
- data/app/views/solid_observer/cache_dashboard/index.html.erb +62 -0
- data/app/views/solid_observer/cache_operations/_confirm_clear.html.erb +6 -0
- data/app/views/solid_observer/cache_operations/index.html.erb +60 -0
- data/app/views/solid_observer/dashboard/_chart.html.erb +28 -0
- data/app/views/solid_observer/dashboard/_live_state.html.erb +20 -0
- data/app/views/solid_observer/dashboard/_queue_table.html.erb +34 -0
- data/app/views/solid_observer/dashboard/_right_now.html.erb +3 -0
- data/app/views/solid_observer/dashboard/_throughput.html.erb +32 -0
- data/app/views/solid_observer/dashboard/index.html.erb +143 -0
- data/app/views/solid_observer/errors/storage_unavailable.html.erb +27 -0
- data/app/views/solid_observer/events/index.html.erb +53 -0
- data/app/views/solid_observer/events/show.html.erb +47 -0
- data/app/views/solid_observer/jobs/index.html.erb +61 -0
- data/app/views/solid_observer/jobs/show.html.erb +71 -0
- data/app/views/solid_observer/shared/_empty_state.html.erb +5 -0
- data/app/views/solid_observer/shared/_pagination.html.erb +17 -0
- data/app/views/solid_observer/shared/_stat_card.html.erb +9 -0
- data/app/views/solid_observer/storages/show.html.erb +71 -0
- data/bin/quality_gate +95 -0
- data/config/routes.rb +22 -0
- data/db/migrate/20260424000001_add_composite_indexes_to_queue_events.rb +30 -0
- data/db/migrate/20260601000001_create_solid_observer_cache_events.rb +22 -0
- data/db/migrate/20260601000002_create_solid_observer_cache_metrics.rb +18 -0
- data/db/migrate/20260602000001_add_component_to_solid_observer_storage_infos.rb +8 -0
- data/lib/generators/solid_observer/install_generator.rb +12 -25
- data/lib/generators/solid_observer/templates/initializer.rb.tt +6 -6
- data/lib/solid_observer/base_metric.rb +1 -1
- data/lib/solid_observer/cache_event_buffer.rb +53 -0
- data/lib/solid_observer/cache_subscriber.rb +47 -0
- data/lib/solid_observer/chart_buffer.rb +83 -0
- data/lib/solid_observer/cli/base.rb +2 -2
- data/lib/solid_observer/cli/jobs.rb +2 -2
- data/lib/solid_observer/cli/status.rb +20 -2
- data/lib/solid_observer/cli/storage.rb +48 -44
- data/lib/solid_observer/configuration.rb +67 -38
- data/lib/solid_observer/correlation_id_resolver.rb +8 -6
- data/lib/solid_observer/engine.rb +110 -18
- data/lib/solid_observer/params/events_filter.rb +37 -0
- data/lib/solid_observer/params/jobs_filter.rb +35 -0
- data/lib/solid_observer/queries/events_query.rb +27 -0
- data/lib/solid_observer/queries/execution_finder.rb +42 -0
- data/lib/solid_observer/queries/job_executions_query.rb +73 -0
- data/lib/solid_observer/queue_event_buffer.rb +163 -25
- data/lib/solid_observer/queue_stats.rb +165 -19
- data/lib/solid_observer/services/cache_operations.rb +115 -0
- data/lib/solid_observer/services/cache_stats.rb +329 -0
- data/lib/solid_observer/services/cleanup_storage.rb +73 -41
- data/lib/solid_observer/services/database_size.rb +91 -0
- data/lib/solid_observer/services/flush_cache_event_buffer.rb +54 -0
- data/lib/solid_observer/services/flush_event_buffer.rb +31 -15
- data/lib/solid_observer/services/install_migrations.rb +49 -0
- data/lib/solid_observer/services/record_cache_event.rb +142 -0
- data/lib/solid_observer/services/record_cache_metric.rb +74 -0
- data/lib/solid_observer/services/record_event.rb +51 -14
- data/lib/solid_observer/services/storage_info_snapshot.rb +128 -0
- data/lib/solid_observer/services/ui_auth_check.rb +65 -0
- data/lib/solid_observer/subscriber.rb +15 -8
- data/lib/solid_observer/version.rb +1 -1
- data/lib/solid_observer.rb +7 -0
- data/lib/tasks/solid_observer.rake +39 -2
- metadata +77 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<% storage_summary = cache_storage_summary(storage_components) %>
|
|
2
|
+
|
|
3
|
+
<section class="so-dashboard-section" aria-labelledby="so-cache-summary-heading">
|
|
4
|
+
<header class="so-dashboard-section__header">
|
|
5
|
+
<h2 id="so-cache-summary-heading" class="so-dashboard-section__title">Summary in selected range</h2>
|
|
6
|
+
</header>
|
|
7
|
+
|
|
8
|
+
<div class="so-stat-cards so-cache-dashboard__summary">
|
|
9
|
+
<article class="so-card so-metric">
|
|
10
|
+
<div class="so-card__label">Hit rate</div>
|
|
11
|
+
<div class="so-metric__value"><%= cache_ratio_percent(stats[:hit_rate]) %></div>
|
|
12
|
+
<div class="so-card__subtitle">hits / read outcomes</div>
|
|
13
|
+
</article>
|
|
14
|
+
|
|
15
|
+
<article class="so-card so-metric">
|
|
16
|
+
<div class="so-card__label">Operations</div>
|
|
17
|
+
<div class="so-metric__value"><%= number_with_delimiter(stats[:operations_count].to_i) %></div>
|
|
18
|
+
<div class="so-card__subtitle">selected window</div>
|
|
19
|
+
</article>
|
|
20
|
+
|
|
21
|
+
<article class="<%= ["so-card", "so-metric", ("so-card--accent-danger" if stats[:errors_count].to_i.positive?)].compact.join(" ") %>">
|
|
22
|
+
<div class="so-card__label">Error rate</div>
|
|
23
|
+
<div class="so-metric__value"><%= cache_ratio_percent(stats[:error_rate]) %></div>
|
|
24
|
+
<div class="so-card__subtitle">errors / operations</div>
|
|
25
|
+
</article>
|
|
26
|
+
|
|
27
|
+
<article class="so-card so-metric">
|
|
28
|
+
<div class="so-card__label">Avg duration</div>
|
|
29
|
+
<div class="so-metric__value"><%= format_duration(stats[:avg_duration].to_f) %></div>
|
|
30
|
+
<div class="so-card__subtitle">operation latency</div>
|
|
31
|
+
</article>
|
|
32
|
+
|
|
33
|
+
<article class="so-card so-metric">
|
|
34
|
+
<div class="so-card__label">Storage footprint</div>
|
|
35
|
+
<div class="so-metric__value"><%= storage_summary[:value] %></div>
|
|
36
|
+
<div class="so-card__subtitle"><%= storage_summary[:subtitle] %></div>
|
|
37
|
+
</article>
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<% content_for :title, "Cache overview" %>
|
|
2
|
+
<% status_class = @cache_dashboard_available ? "so-badge--success" : "so-badge--warning" %>
|
|
3
|
+
|
|
4
|
+
<div class="so-dashboard so-cache-dashboard">
|
|
5
|
+
<span class="sr-only">Cache Dashboard</span>
|
|
6
|
+
|
|
7
|
+
<div class="so-content__header">
|
|
8
|
+
<h1>Cache overview</h1>
|
|
9
|
+
|
|
10
|
+
<% if @cache_dashboard_available %>
|
|
11
|
+
<div class="so-cache-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-cache-dashboard__hint">Selected range: <%= cache_range_label(@range) %> · keys and values are never shown.</p>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<% if @cache_dashboard_available %>
|
|
24
|
+
<form action="<%= request.path %>" method="get" class="so-dashboard-toolbar so-cache-dashboard__range-form">
|
|
25
|
+
<label for="so-cache-range" class="so-card__label so-filters__label">Range</label>
|
|
26
|
+
<select id="so-cache-range" name="range" onchange="this.form.submit()">
|
|
27
|
+
<% SolidObserver::Services::CacheStats::RANGES.each_key do |key| %>
|
|
28
|
+
<option value="<%= key %>" <%= "selected" if key == @range %>><%= cache_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/cache_dashboard/summary", stats: @stats, storage_components: @storage_components %>
|
|
35
|
+
<%= render "solid_observer/cache_dashboard/charts" %>
|
|
36
|
+
|
|
37
|
+
<% if @stability&.[](:available) %>
|
|
38
|
+
<section class="so-stability" data-so-zone="cache-stability" aria-labelledby="so-cache-stability-heading">
|
|
39
|
+
<span class="so-stability__label" id="so-cache-stability-heading">Stability</span>
|
|
40
|
+
<%= cache_stability_badge(@stability[:state]) %>
|
|
41
|
+
<span class="so-stability__detail"><%= cache_stability_detail(@stability) %></span>
|
|
42
|
+
<a href="#so-cache-events-heading" class="so-stability__link">View sampled events →</a>
|
|
43
|
+
</section>
|
|
44
|
+
<% end %>
|
|
45
|
+
|
|
46
|
+
<%= render "solid_observer/cache_dashboard/recent_events", recent_events: @recent_events %>
|
|
47
|
+
<% else %>
|
|
48
|
+
<section class="so-card so-card--section" aria-labelledby="so-cache-unavailable-heading">
|
|
49
|
+
<header class="so-dashboard-section__header">
|
|
50
|
+
<h2 id="so-cache-unavailable-heading" class="so-dashboard-section__title">Cache dashboard unavailable</h2>
|
|
51
|
+
<span class="so-badge so-badge--pill <%= status_class %>">
|
|
52
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true">
|
|
53
|
+
<circle r="3" cx="3" cy="3" />
|
|
54
|
+
</svg>
|
|
55
|
+
Unavailable
|
|
56
|
+
</span>
|
|
57
|
+
</header>
|
|
58
|
+
|
|
59
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--idle">Cache dashboard is unavailable because SolidCache support is disabled or not detected. Metrics are unavailable.</p>
|
|
60
|
+
</section>
|
|
61
|
+
<% end %>
|
|
62
|
+
</div>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<% content_for :title, "Cache controls" %>
|
|
2
|
+
<% cache_operations = SolidObserver::Services::CacheOperations %>
|
|
3
|
+
<% status_class = @cache_controls_available ? "so-badge--success" : "so-badge--warning" %>
|
|
4
|
+
|
|
5
|
+
<div class="so-dashboard so-cache-controls">
|
|
6
|
+
<div class="so-content__header">
|
|
7
|
+
<h1>Cache controls</h1>
|
|
8
|
+
<div class="so-cache-controls__intro">
|
|
9
|
+
<span class="so-badge so-badge--pill <%= status_class %>">
|
|
10
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true">
|
|
11
|
+
<circle r="3" cx="3" cy="3" />
|
|
12
|
+
</svg>
|
|
13
|
+
<%= @cache_controls_available ? "Available" : "Unavailable" %>
|
|
14
|
+
</span>
|
|
15
|
+
<p class="so-cache-controls__hint">Clear or prune SolidCache safely. Cache keys and values are never shown.</p>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<% if @cache_controls_available %>
|
|
20
|
+
<section class="so-card so-card--section" aria-labelledby="so-cache-controls-title">
|
|
21
|
+
<header class="so-dashboard-section__header">
|
|
22
|
+
<h2 id="so-cache-controls-title" class="so-dashboard-section__title">Operational controls</h2>
|
|
23
|
+
</header>
|
|
24
|
+
|
|
25
|
+
<div class="so-cache-control-row">
|
|
26
|
+
<div class="so-cache-control-row__copy">
|
|
27
|
+
<h3 class="so-cache-control-row__title">Prune expired entries</h3>
|
|
28
|
+
<p class="so-cache-control-row__body">Removes expired SolidCache records only. Active cache entries remain eligible for hits.</p>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="so-cache-control-row__action">
|
|
32
|
+
<%= button_to "Prune expired entries",
|
|
33
|
+
prune_cache_operations_path,
|
|
34
|
+
method: :post,
|
|
35
|
+
class: "so-btn",
|
|
36
|
+
form: {class: "so-form--inline"} %>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="so-cache-control-row">
|
|
41
|
+
<div class="so-cache-control-row__copy">
|
|
42
|
+
<h3 class="so-cache-control-row__title">Clear all cache</h3>
|
|
43
|
+
<p class="so-cache-control-row__body">Evicts all cached application data. Requests may slow while the cache rebuilds.</p>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="so-cache-control-row__action">
|
|
47
|
+
<%= render "confirm_clear" %>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</section>
|
|
51
|
+
<% else %>
|
|
52
|
+
<section class="so-card so-card--section" aria-labelledby="so-cache-controls-unavailable-title">
|
|
53
|
+
<header class="so-dashboard-section__header">
|
|
54
|
+
<h2 id="so-cache-controls-unavailable-title" class="so-dashboard-section__title">Operational controls</h2>
|
|
55
|
+
</header>
|
|
56
|
+
|
|
57
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--idle"><%= cache_operations.unavailable_message %></p>
|
|
58
|
+
</section>
|
|
59
|
+
<% end %>
|
|
60
|
+
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<div class="so-chart-strip" data-so-zone="chart" data-so-chart>
|
|
2
|
+
<% if SolidObserver.config.persistence_mode? %>
|
|
3
|
+
<figure class="so-spark" data-so-spark="performed">
|
|
4
|
+
<figcaption class="so-spark__label">Performed total <span data-so-range-copy><%= range_label(local_assigns[:range] || "15m") %></span></figcaption>
|
|
5
|
+
<svg class="so-spark__svg" viewBox="0 0 120 32" preserveAspectRatio="none" aria-hidden="true">
|
|
6
|
+
<line class="so-spark__baseline" x1="1" x2="119" y1="31" y2="31"/>
|
|
7
|
+
<polyline class="so-spark__line" points="<%= spark_points(@chart[:performed]) %>"/>
|
|
8
|
+
</svg>
|
|
9
|
+
<span class="so-spark__value" data-so-card-value="performed_in_range"><%= number_with_delimiter(@stats[:performed_in_range].to_i) %></span>
|
|
10
|
+
</figure>
|
|
11
|
+
<figure class="so-spark" data-so-spark="failed">
|
|
12
|
+
<figcaption class="so-spark__label">Failed total <span data-so-range-copy><%= range_label(local_assigns[:range] || "15m") %></span></figcaption>
|
|
13
|
+
<svg class="so-spark__svg" viewBox="0 0 120 32" preserveAspectRatio="none" aria-hidden="true">
|
|
14
|
+
<line class="so-spark__baseline" x1="1" x2="119" y1="31" y2="31"/>
|
|
15
|
+
<polyline class="so-spark__line" points="<%= spark_points(@chart[:failed]) %>"/>
|
|
16
|
+
</svg>
|
|
17
|
+
<span class="so-spark__value" data-so-card-value="failed_in_range"><%= number_with_delimiter(@stats[:failed_in_range].to_i) %></span>
|
|
18
|
+
</figure>
|
|
19
|
+
<% end %>
|
|
20
|
+
<figure class="so-spark" data-so-spark="ready">
|
|
21
|
+
<figcaption class="so-spark__label">Ready depth</figcaption>
|
|
22
|
+
<svg class="so-spark__svg" viewBox="0 0 120 32" preserveAspectRatio="none" aria-hidden="true">
|
|
23
|
+
<line class="so-spark__baseline" x1="1" x2="119" y1="31" y2="31"/>
|
|
24
|
+
<polyline class="so-spark__line" points="<%= spark_points(@chart[:ready]) %>"/>
|
|
25
|
+
</svg>
|
|
26
|
+
<span class="so-spark__value" data-so-card-value="ready"><%= number_with_delimiter(@stats[:ready].to_i) %></span>
|
|
27
|
+
</figure>
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<%# Zone B: Right now — live state cards %>
|
|
2
|
+
<% if stats[:available] %>
|
|
3
|
+
<div class="so-stat-cards" data-so-zone="live-state">
|
|
4
|
+
<%= render "solid_observer/shared/stat_card", label: "Ready", subtitle: "queued", value: stats[:ready], data_key: "ready" %>
|
|
5
|
+
<%= render "solid_observer/shared/stat_card", label: "Scheduled", subtitle: "future runs", value: stats[:scheduled], data_key: "scheduled" %>
|
|
6
|
+
<%= render "solid_observer/shared/stat_card", label: "In-flight", subtitle: "in progress", value: stats[:claimed], data_key: "claimed" %>
|
|
7
|
+
<%= render "solid_observer/shared/stat_card", label: "Workers", subtitle: "active processes", value: stats[:workers], data_key: "workers" %>
|
|
8
|
+
<%= render "solid_observer/shared/stat_card", label: "Pending failures", subtitle: "awaiting retry", value: stats[:failed], data_key: "failed" %>
|
|
9
|
+
</div>
|
|
10
|
+
<% if stats[:ready].to_i.zero? && stats[:scheduled].to_i.zero? && stats[:claimed].to_i.zero? && stats[:failed].to_i.zero? %>
|
|
11
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--idle">Queue idle right now. No ready, scheduled, in-flight, or pending-failure work is currently visible.</p>
|
|
12
|
+
<% end %>
|
|
13
|
+
<% else %>
|
|
14
|
+
<div class="so-card">
|
|
15
|
+
<div class="so-empty">
|
|
16
|
+
<div class="so-empty__icon">⚠️</div>
|
|
17
|
+
<div>SolidQueue is not available</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<%# Zone E: Per-queue table with live depth and range throughput %>
|
|
2
|
+
<% if persistence_mode? && stats[:available] %>
|
|
3
|
+
<div class="so-card so-card--section" data-so-zone="queue-table">
|
|
4
|
+
<div class="so-card__label">Queue throughput</div>
|
|
5
|
+
<% queues = stats[:queues] || {} %>
|
|
6
|
+
<% performed_by_queue = stats[:performed_by_queue] || {} %>
|
|
7
|
+
<% failed_by_queue = stats[:failed_by_queue] || {} %>
|
|
8
|
+
<% all_queue_names = (queues.keys | performed_by_queue.keys | failed_by_queue.keys).sort %>
|
|
9
|
+
<% if all_queue_names.any? %>
|
|
10
|
+
<table class="so-table so-table--card">
|
|
11
|
+
<thead>
|
|
12
|
+
<tr>
|
|
13
|
+
<th>Queue</th>
|
|
14
|
+
<th>Live depth</th>
|
|
15
|
+
<th>Performed <span data-so-range-copy><%= range_label(range) %></span></th>
|
|
16
|
+
<th>Failed <span data-so-range-copy><%= range_label(range) %></span></th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<% all_queue_names.each do |queue_name| %>
|
|
21
|
+
<tr>
|
|
22
|
+
<td><%= queue_name %></td>
|
|
23
|
+
<td data-so-table-value="queue-depth-<%= queue_name %>"><%= number_with_delimiter(queues[queue_name].to_i) %></td>
|
|
24
|
+
<td data-so-table-value="queue-performed-<%= queue_name %>"><%= number_with_delimiter(performed_by_queue[queue_name].to_i) %></td>
|
|
25
|
+
<td data-so-table-value="queue-failed-<%= queue_name %>"><%= number_with_delimiter(failed_by_queue[queue_name].to_i) %></td>
|
|
26
|
+
</tr>
|
|
27
|
+
<% end %>
|
|
28
|
+
</tbody>
|
|
29
|
+
</table>
|
|
30
|
+
<% else %>
|
|
31
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--empty-queue">No queue activity in this range. Live depth still shows current waiting work when present.</p>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
|
34
|
+
<% end %>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<%# Zone C: Throughput in selected range %>
|
|
2
|
+
<% if persistence_mode? && stats[:available] %>
|
|
3
|
+
<div class="so-stat-cards" data-so-zone="throughput">
|
|
4
|
+
<div class="so-card so-metric">
|
|
5
|
+
<div class="so-card__label">Performed</div>
|
|
6
|
+
<div class="so-metric__value" data-so-card-value="performed_in_range"><%= number_with_delimiter(stats[:performed_in_range].to_i) %></div>
|
|
7
|
+
<span class="so-metric__suffix" data-so-card-suffix>jobs</span>
|
|
8
|
+
<div class="so-card__subtitle" data-so-range-copy><%= range_label(range) %></div>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="so-card so-metric">
|
|
11
|
+
<div class="so-card__label">Failed</div>
|
|
12
|
+
<div class="so-metric__value" data-so-card-value="failed_in_range"><%= number_with_delimiter(stats[:failed_in_range].to_i) %></div>
|
|
13
|
+
<span class="so-metric__suffix" data-so-card-suffix>jobs</span>
|
|
14
|
+
<div class="so-card__subtitle" data-so-range-copy><%= range_label(range) %></div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="so-card so-metric">
|
|
17
|
+
<div class="so-card__label">Enqueued</div>
|
|
18
|
+
<div class="so-metric__value" data-so-card-value="enqueued_in_range"><%= number_with_delimiter(stats[:enqueued_in_range].to_i) %></div>
|
|
19
|
+
<span class="so-metric__suffix" data-so-card-suffix>jobs</span>
|
|
20
|
+
<div class="so-card__subtitle" data-so-range-copy><%= range_label(range) %></div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="so-card so-metric">
|
|
23
|
+
<div class="so-card__label">Avg duration</div>
|
|
24
|
+
<div class="so-metric__value" data-so-card-value="avg_duration_in_range"><%= stats[:avg_duration_in_range] > 0 ? number_with_delimiter((stats[:avg_duration_in_range] * 1000).round) : "0" %></div>
|
|
25
|
+
<span class="so-metric__suffix" data-so-card-suffix>ms</span>
|
|
26
|
+
<div class="so-card__subtitle" data-so-range-copy><%= range_label(range) %></div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<% if stats[:performed_in_range].to_i.zero? && stats[:failed_in_range].to_i.zero? && stats[:enqueued_in_range].to_i.zero? %>
|
|
30
|
+
<p class="so-dashboard-section__meta so-dashboard-section__meta--empty-range"><%= "No throughput #{range_label(range).sub('in ', '')}".html_safe %>. Jobs may still be waiting right now; this row only counts completed queue events in the selected range.</p>
|
|
31
|
+
<% end %>
|
|
32
|
+
<% end %>
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<% content_for :title, "Dashboard" %>
|
|
2
|
+
|
|
3
|
+
<% if @component == "cache" %>
|
|
4
|
+
<%= render template: "solid_observer/cache_dashboard/index" %>
|
|
5
|
+
<% else %>
|
|
6
|
+
<div class="so-dashboard">
|
|
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>
|
|
143
|
+
<% end %>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<% content_for :title, "Storage Unavailable" %>
|
|
2
|
+
|
|
3
|
+
<div class="so-content__header">
|
|
4
|
+
<h1>SolidObserver storage is not reachable</h1>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div class="so-card so-card--spaced so-card--accent-danger">
|
|
8
|
+
<p class="so-card__label so-card__label--danger">
|
|
9
|
+
<strong><%= @error_class %></strong>
|
|
10
|
+
</p>
|
|
11
|
+
<pre class="so-pre so-pre--error"><%= @error_message %></pre>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="so-card so-card--spaced">
|
|
15
|
+
<h2>What to do next</h2>
|
|
16
|
+
<ol>
|
|
17
|
+
<li>Confirm the <code>solid_observer_queue</code> database in <code>config/database.yml</code> is reachable.</li>
|
|
18
|
+
<li>
|
|
19
|
+
Run migrations if you have not yet:
|
|
20
|
+
<pre class="so-pre">bin/rails solid_observer:install:migrations
|
|
21
|
+
bin/rails db:create
|
|
22
|
+
bin/rails db:migrate</pre>
|
|
23
|
+
</li>
|
|
24
|
+
<li>If the database server is temporarily down, reload this page once it recovers.</li>
|
|
25
|
+
<li>Switch to <code>storage_mode: :realtime</code> in <code>config/initializers/solid_observer.rb</code> if you want SolidObserver without a persistence database.</li>
|
|
26
|
+
</ol>
|
|
27
|
+
</div>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<% content_for :title, "Events" %>
|
|
2
|
+
|
|
3
|
+
<div class="so-content__header">
|
|
4
|
+
<h1>Events</h1>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<%= form_tag events_path, method: :get, class: "so-filters" do %>
|
|
8
|
+
<%= select_tag :event_type,
|
|
9
|
+
options_for_select([["All Event Types", ""]] + @available_event_types.map { |t| [t.humanize, t] }, @event_type),
|
|
10
|
+
onchange: "this.form.submit()" %>
|
|
11
|
+
|
|
12
|
+
<%= select_tag :job_class,
|
|
13
|
+
options_for_select([["All Classes", ""]] + @available_job_classes.map { |c| [c, c] }, @job_class),
|
|
14
|
+
onchange: "this.form.submit()" %>
|
|
15
|
+
|
|
16
|
+
<%= select_tag :queue_name,
|
|
17
|
+
options_for_select([["All Queues", ""]] + @available_queues.map { |q| [q, q] }, @queue_name),
|
|
18
|
+
onchange: "this.form.submit()" %>
|
|
19
|
+
|
|
20
|
+
<%= date_field_tag :from, @from&.strftime("%Y-%m-%d"), placeholder: "From date", onchange: "this.form.submit()" %>
|
|
21
|
+
<%= date_field_tag :to, @to&.strftime("%Y-%m-%d"), placeholder: "To date", onchange: "this.form.submit()" %>
|
|
22
|
+
|
|
23
|
+
<%= link_to "Clear Filters", events_path, class: "so-btn" if @event_type.present? || @job_class.present? || @queue_name.present? || @from || @to %>
|
|
24
|
+
<% end %>
|
|
25
|
+
|
|
26
|
+
<% if @events.any? %>
|
|
27
|
+
<table class="so-table so-table--listing">
|
|
28
|
+
<thead>
|
|
29
|
+
<tr>
|
|
30
|
+
<th>Event</th>
|
|
31
|
+
<th>Job Class</th>
|
|
32
|
+
<th>Queue</th>
|
|
33
|
+
<th>Duration</th>
|
|
34
|
+
<th>Time</th>
|
|
35
|
+
</tr>
|
|
36
|
+
</thead>
|
|
37
|
+
<tbody>
|
|
38
|
+
<% @events.each do |event| %>
|
|
39
|
+
<tr>
|
|
40
|
+
<td><%= link_to event.event_type.humanize, event_path(event.id) %></td>
|
|
41
|
+
<td><%= event.job_class %></td>
|
|
42
|
+
<td><%= event.queue_name %></td>
|
|
43
|
+
<td><%= duration_with_semantic(event.duration, event.event_type) %></td>
|
|
44
|
+
<td><%= time_ago_in_words(event.recorded_at) %> ago</td>
|
|
45
|
+
</tr>
|
|
46
|
+
<% end %>
|
|
47
|
+
</tbody>
|
|
48
|
+
</table>
|
|
49
|
+
|
|
50
|
+
<%= render "solid_observer/shared/pagination", page: @page, total_pages: @total_pages %>
|
|
51
|
+
<% else %>
|
|
52
|
+
<%= render "solid_observer/shared/empty_state", message: "No events found matching the criteria" %>
|
|
53
|
+
<% end %>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<% content_for :title, "Event ##{@event.id}" %>
|
|
2
|
+
|
|
3
|
+
<div class="so-content__header">
|
|
4
|
+
<h1>Event #<%= @event.id %></h1>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div class="so-back-link">
|
|
8
|
+
<%= link_to "← Back to Events", events_path(event_type: params[:event_type], job_class: params[:job_class], queue_name: params[:queue_name], from: params[:from], to: params[:to]), class: "so-btn" %>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div class="so-card so-card--spaced">
|
|
12
|
+
<table class="so-table so-details">
|
|
13
|
+
<tbody>
|
|
14
|
+
<tr>
|
|
15
|
+
<td>Event Type</td>
|
|
16
|
+
<td><%= @event.event_type.humanize %></td>
|
|
17
|
+
</tr>
|
|
18
|
+
<tr>
|
|
19
|
+
<td>Job Class</td>
|
|
20
|
+
<td><code><%= @event.job_class %></code></td>
|
|
21
|
+
</tr>
|
|
22
|
+
<tr>
|
|
23
|
+
<td>Queue</td>
|
|
24
|
+
<td><%= @event.queue_name %></td>
|
|
25
|
+
</tr>
|
|
26
|
+
<tr>
|
|
27
|
+
<td>Correlation ID</td>
|
|
28
|
+
<td><code><%= @event.correlation_id || "N/A" %></code></td>
|
|
29
|
+
</tr>
|
|
30
|
+
<tr>
|
|
31
|
+
<td>Duration</td>
|
|
32
|
+
<td><%= duration_with_semantic(@event.duration, @event.event_type) %></td>
|
|
33
|
+
</tr>
|
|
34
|
+
<tr>
|
|
35
|
+
<td>Recorded At</td>
|
|
36
|
+
<td><%= @event.recorded_at.strftime("%Y-%m-%d %H:%M:%S.%3N UTC") %></td>
|
|
37
|
+
</tr>
|
|
38
|
+
</tbody>
|
|
39
|
+
</table>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<% if @metadata %>
|
|
43
|
+
<div class="so-card so-card--spaced">
|
|
44
|
+
<div class="so-card__label">Metadata</div>
|
|
45
|
+
<pre class="so-pre"><code><%= JSON.pretty_generate(@metadata) %></code></pre>
|
|
46
|
+
</div>
|
|
47
|
+
<% end %>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<% content_for :title, "Jobs" %>
|
|
2
|
+
|
|
3
|
+
<div class="so-content__header">
|
|
4
|
+
<h1>Jobs</h1>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<%= form_tag jobs_path, method: :get, class: "so-filters" do %>
|
|
8
|
+
<%= select_tag :status,
|
|
9
|
+
options_for_select([["All Active", "all_active"], ["Ready", "ready"], ["Scheduled", "scheduled"], ["Claimed", "claimed"], ["Failed", "failed"]], @status),
|
|
10
|
+
onchange: "this.form.submit()" %>
|
|
11
|
+
|
|
12
|
+
<%= select_tag :queue_name,
|
|
13
|
+
options_for_select([["All Queues", ""]] + @available_queues.map { |q| [q, q] }, @queue_name),
|
|
14
|
+
onchange: "this.form.submit()" %>
|
|
15
|
+
|
|
16
|
+
<%= select_tag :job_class,
|
|
17
|
+
options_for_select([["All Classes", ""]] + @available_job_classes.map { |c| [c, c] }, @job_class),
|
|
18
|
+
onchange: "this.form.submit()" %>
|
|
19
|
+
<% end %>
|
|
20
|
+
|
|
21
|
+
<% if @jobs.any? %>
|
|
22
|
+
<table class="so-table so-table--listing">
|
|
23
|
+
<thead>
|
|
24
|
+
<tr>
|
|
25
|
+
<th>ID</th>
|
|
26
|
+
<th>Class</th>
|
|
27
|
+
<th>Queue</th>
|
|
28
|
+
<th>Status</th>
|
|
29
|
+
<th>Created</th>
|
|
30
|
+
<th>Actions</th>
|
|
31
|
+
</tr>
|
|
32
|
+
</thead>
|
|
33
|
+
<tbody>
|
|
34
|
+
<% @jobs.each do |execution| %>
|
|
35
|
+
<% job = execution.job %>
|
|
36
|
+
<% status = execution_status(execution) %>
|
|
37
|
+
<tr>
|
|
38
|
+
<td><%= link_to execution.id, job_path(execution.id, status: status) %></td>
|
|
39
|
+
<td><%= job&.class_name || "N/A" %></td>
|
|
40
|
+
<td><%= job&.queue_name || execution&.queue_name || "N/A" %></td>
|
|
41
|
+
<td><%= status_badge(status) %></td>
|
|
42
|
+
<td><%= time_ago_in_words(execution.created_at) %> ago</td>
|
|
43
|
+
<td>
|
|
44
|
+
<%= link_to "View", job_path(execution.id, status: status), class: "so-btn" %>
|
|
45
|
+
<% if execution.is_a?(SolidQueue::FailedExecution) %>
|
|
46
|
+
<%= button_to "Retry", retry_job_path(execution.id), method: :post, class: "so-btn so-btn--primary", form: { class: "so-form--inline" }, data: { confirm: "Retry job #{execution.id}?" } %>
|
|
47
|
+
<%= button_to "Discard", discard_job_path(execution.id), method: :post, class: "so-btn so-btn--danger", form: { class: "so-form--inline" }, data: { confirm: "Discard job #{execution.id}? This cannot be undone." } %>
|
|
48
|
+
<% end %>
|
|
49
|
+
</td>
|
|
50
|
+
</tr>
|
|
51
|
+
<% end %>
|
|
52
|
+
</tbody>
|
|
53
|
+
</table>
|
|
54
|
+
|
|
55
|
+
<%= render "solid_observer/shared/pagination", page: @page, total_pages: @total_pages %>
|
|
56
|
+
<% else %>
|
|
57
|
+
<%= render "solid_observer/shared/empty_state",
|
|
58
|
+
message: persistence_mode? ?
|
|
59
|
+
"No jobs match this filter. Successfully completed jobs are not stored here — see #{link_to("Events tab", events_path)} for the full job history.".html_safe :
|
|
60
|
+
"No jobs match this filter." %>
|
|
61
|
+
<% end %>
|