solid_observer 0.3.0 → 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 +21 -0
- data/README.md +134 -81
- data/app/assets/stylesheets/solid_observer/application.css +18 -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 +44 -1
- data/app/controllers/solid_observer/storages_controller.rb +1 -1
- data/app/helpers/solid_observer/application_helper.rb +154 -5
- data/app/helpers/solid_observer/dashboard_helper.rb +30 -11
- 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/storage_info.rb +4 -1
- data/app/views/layouts/solid_observer/application.html.erb +144 -17
- 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/index.html.erb +34 -4
- data/app/views/solid_observer/jobs/show.html.erb +3 -3
- data/app/views/solid_observer/storages/show.html.erb +64 -32
- data/bin/quality_gate +1 -1
- data/config/routes.rb +5 -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/templates/initializer.rb.tt +2 -1
- data/lib/solid_observer/cache_event_buffer.rb +53 -0
- data/lib/solid_observer/cache_subscriber.rb +47 -0
- data/lib/solid_observer/cli/storage.rb +16 -13
- data/lib/solid_observer/configuration.rb +22 -3
- data/lib/solid_observer/engine.rb +44 -7
- 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 +18 -2
- data/lib/solid_observer/services/database_size.rb +13 -8
- data/lib/solid_observer/services/flush_cache_event_buffer.rb +54 -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/storage_info_snapshot.rb +128 -0
- data/lib/solid_observer/version.rb +1 -1
- data/lib/tasks/solid_observer.rake +29 -0
- metadata +23 -1
|
@@ -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>
|
|
@@ -1,18 +1,46 @@
|
|
|
1
1
|
<% content_for :title, "Dashboard" %>
|
|
2
2
|
|
|
3
|
+
<% if @component == "cache" %>
|
|
4
|
+
<%= render template: "solid_observer/cache_dashboard/index" %>
|
|
5
|
+
<% else %>
|
|
3
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 %>
|
|
4
31
|
<%# Zone A: Toolbar — range selector, Refresh, Live toggle, help disclosure %>
|
|
5
32
|
<div class="so-dashboard-toolbar" data-so-live>
|
|
6
|
-
<
|
|
33
|
+
<form action="<%= request.path %>" method="get" class="so-dashboard-toolbar__left">
|
|
7
34
|
<label for="so-range" class="so-card__label so-filters__label">Range</label>
|
|
8
|
-
<select id="so-range" name="range" data-so-range-select>
|
|
35
|
+
<select id="so-range" name="range" data-so-range-select onchange="this.form.submit()">
|
|
9
36
|
<% SolidObserver::QueueStats::RANGES.each_key do |key| %>
|
|
10
37
|
<option value="<%= key %>" <%= "selected" if key == @range %>><%= key %></option>
|
|
11
38
|
<% end %>
|
|
12
39
|
</select>
|
|
13
40
|
<button type="button" class="so-btn so-btn--refresh" data-so-refresh aria-busy="false">Refresh data</button>
|
|
14
41
|
<span class="so-toolbar-freshness" data-so-freshness aria-live="polite"></span>
|
|
15
|
-
|
|
42
|
+
<input type="hidden" name="live" value="<%= @live ? 'on' : '' %>">
|
|
43
|
+
</form>
|
|
16
44
|
<div class="so-dashboard-toolbar__right">
|
|
17
45
|
<label class="so-toggle so-toggle--pill <%= "so-toggle--on" if @live %>">
|
|
18
46
|
<input type="checkbox" name="live" value="on" <%= "checked" if @live %> data-so-live-toggle>
|
|
@@ -110,4 +138,6 @@
|
|
|
110
138
|
</div>
|
|
111
139
|
<% end %>
|
|
112
140
|
<% end %>
|
|
113
|
-
|
|
141
|
+
<% end %>
|
|
142
|
+
</div>
|
|
143
|
+
<% end %>
|
|
@@ -52,16 +52,16 @@
|
|
|
52
52
|
<tbody>
|
|
53
53
|
<tr>
|
|
54
54
|
<td>Error Class</td>
|
|
55
|
-
<td><code><%= @execution.error
|
|
55
|
+
<td><code><%= @execution.error["exception_class"] %></code></td>
|
|
56
56
|
</tr>
|
|
57
57
|
<tr>
|
|
58
58
|
<td>Message</td>
|
|
59
|
-
<td><%= @execution.error
|
|
59
|
+
<td><%= @execution.error["message"] %></td>
|
|
60
60
|
</tr>
|
|
61
61
|
</tbody>
|
|
62
62
|
</table>
|
|
63
63
|
<div class="so-card__label">Backtrace</div>
|
|
64
|
-
<pre class="so-pre so-pre--error"><code><%= @execution.error
|
|
64
|
+
<pre class="so-pre so-pre--error"><code><%= @execution.error["backtrace"]&.first(20)&.join("\n") || "No backtrace available" %></code></pre>
|
|
65
65
|
</div>
|
|
66
66
|
|
|
67
67
|
<div class="so-actions">
|
|
@@ -1,39 +1,71 @@
|
|
|
1
1
|
<% content_for :title, "Storage" %>
|
|
2
2
|
|
|
3
|
-
<div class="so-
|
|
4
|
-
<
|
|
5
|
-
</
|
|
6
|
-
|
|
7
|
-
<% if @current_storage %>
|
|
8
|
-
<div class="so-stat-cards">
|
|
9
|
-
<%= render "solid_observer/shared/stat_card", label: "Database Size", value: number_to_human_size(@current_storage.db_size_bytes, precision: 1, significant: false, strip_insignificant_zeros: false) %>
|
|
10
|
-
<%= render "solid_observer/shared/stat_card", label: "Event Count", value: number_with_delimiter(@current_storage.event_count) %>
|
|
11
|
-
<%= render "solid_observer/shared/stat_card", label: "Last Updated", value: time_ago_in_words(@current_storage.recorded_at) + " ago" %>
|
|
3
|
+
<div class="so-dashboard">
|
|
4
|
+
<div class="so-content__header">
|
|
5
|
+
<h1>Storage</h1>
|
|
12
6
|
</div>
|
|
13
7
|
|
|
14
|
-
<% if @
|
|
15
|
-
<
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
8
|
+
<% if @storage_components.any? %>
|
|
9
|
+
<section class="so-dashboard-section" aria-labelledby="component-health-title">
|
|
10
|
+
<header class="so-dashboard-section__header">
|
|
11
|
+
<h2 id="component-health-title" class="so-dashboard-section__title">Component health</h2>
|
|
12
|
+
</header>
|
|
13
|
+
|
|
14
|
+
<div class="so-stat-cards">
|
|
15
|
+
<% @storage_components.each do |component| %>
|
|
16
|
+
<% status_class = component[:available] ? "so-badge--success" : "so-badge--warning" %>
|
|
17
|
+
<% size_value = component[:available] && component[:db_size_bytes] ? number_to_human_size(component[:db_size_bytes], precision: 1, significant: false, strip_insignificant_zeros: false) : "—" %>
|
|
18
|
+
<% records_value = component[:available] && !component[:event_count].nil? ? number_with_delimiter(component[:event_count]) : "—" %>
|
|
19
|
+
|
|
20
|
+
<article class="so-card">
|
|
21
|
+
<div class="so-card__label"><%= component[:label] %></div>
|
|
22
|
+
<div class="so-card__subtitle">
|
|
23
|
+
<span class="so-badge so-badge--pill <%= status_class %>">
|
|
24
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true"><circle r="3" cx="3" cy="3"/></svg>
|
|
25
|
+
<%= component[:available] ? "Available" : "Unavailable" %>
|
|
26
|
+
</span>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="so-card__value"><%= size_value %></div>
|
|
29
|
+
<div class="so-card__subtitle">
|
|
30
|
+
<% if component[:available] %>
|
|
31
|
+
<%= records_value %> <%= component[:record_label] %>
|
|
32
|
+
<% else %>
|
|
33
|
+
<%= component[:unavailable_reason] %>
|
|
34
|
+
<% end %>
|
|
35
|
+
</div>
|
|
36
|
+
</article>
|
|
37
|
+
<% end %>
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|
|
40
|
+
|
|
41
|
+
<% if @storage_history.any? %>
|
|
42
|
+
<div class="so-card so-card--section">
|
|
43
|
+
<div class="so-card__label">Recent Snapshots</div>
|
|
44
|
+
<table class="so-table so-table--card">
|
|
45
|
+
<caption class="sr-only">Recent storage snapshots by component</caption>
|
|
46
|
+
<thead>
|
|
27
47
|
<tr>
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
<
|
|
48
|
+
<th>Time</th>
|
|
49
|
+
<th>Component</th>
|
|
50
|
+
<th>Size</th>
|
|
51
|
+
<th>Records</th>
|
|
31
52
|
</tr>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
53
|
+
</thead>
|
|
54
|
+
<tbody>
|
|
55
|
+
<% @storage_history.each do |snapshot| %>
|
|
56
|
+
<% record_label = snapshot.component == "solid_cache" ? "cache rows" : "observer events" %>
|
|
57
|
+
<tr>
|
|
58
|
+
<td><%= snapshot.recorded_at.strftime("%Y-%m-%d %H:%M") %></td>
|
|
59
|
+
<td><%= snapshot.component.humanize %></td>
|
|
60
|
+
<td><%= number_to_human_size(snapshot.db_size_bytes, precision: 1, significant: false, strip_insignificant_zeros: false) %></td>
|
|
61
|
+
<td><%= number_with_delimiter(snapshot.event_count) %> <%= record_label %></td>
|
|
62
|
+
</tr>
|
|
63
|
+
<% end %>
|
|
64
|
+
</tbody>
|
|
65
|
+
</table>
|
|
66
|
+
</div>
|
|
67
|
+
<% end %>
|
|
68
|
+
<% else %>
|
|
69
|
+
<%= render "solid_observer/shared/empty_state", message: "No storage information available" %>
|
|
36
70
|
<% end %>
|
|
37
|
-
|
|
38
|
-
<%= render "solid_observer/shared/empty_state", message: "No storage information available" %>
|
|
39
|
-
<% end %>
|
|
71
|
+
</div>
|
data/bin/quality_gate
CHANGED
data/config/routes.rb
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
SolidObserver::Engine.routes.draw do
|
|
4
4
|
root "dashboard#index"
|
|
5
|
+
get "queue", to: "dashboard#index", defaults: {component: "queue"}, as: :queue_dashboard
|
|
6
|
+
get "cache", to: "cache_dashboard#index", as: :cache_dashboard
|
|
7
|
+
get "cache/controls", to: "cache_operations#index", as: :cache_operations
|
|
8
|
+
post "cache/controls/prune", to: "cache_operations#prune", as: :prune_cache_operations
|
|
9
|
+
post "cache/controls/clear", to: "cache_operations#clear", as: :clear_cache_operations
|
|
5
10
|
get "poll_data", to: "dashboard#poll_data", as: :poll_data
|
|
6
11
|
get "live_poll.js", to: "dashboard#live_poll", as: :live_poll_script
|
|
7
12
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateSolidObserverCacheEvents < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :solid_observer_cache_events do |t|
|
|
6
|
+
t.string :event_type, null: false, limit: 64
|
|
7
|
+
t.string :key_digest, null: false, limit: 64
|
|
8
|
+
t.boolean :hit
|
|
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 :key_digest
|
|
18
|
+
t.index :hit
|
|
19
|
+
t.index :error_class
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateSolidObserverCacheMetrics < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :solid_observer_cache_metrics do |t|
|
|
6
|
+
t.string :event_type, null: false, limit: 64
|
|
7
|
+
t.datetime :period_start, null: false
|
|
8
|
+
t.bigint :operations_count, null: false, default: 0
|
|
9
|
+
t.bigint :hits_count, null: false, default: 0
|
|
10
|
+
t.bigint :misses_count, null: false, default: 0
|
|
11
|
+
t.bigint :errors_count, null: false, default: 0
|
|
12
|
+
t.float :duration_total, null: false, default: 0.0
|
|
13
|
+
|
|
14
|
+
t.index [:event_type, :period_start], unique: true, name: "idx_solid_observer_cache_metrics_unique"
|
|
15
|
+
t.index :period_start
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddComponentToSolidObserverStorageInfos < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
add_column :solid_observer_storage_info, :component, :string, null: false, default: "queue_observer"
|
|
6
|
+
add_index :solid_observer_storage_info, :component
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -19,7 +19,8 @@ SolidObserver.configure do |config|
|
|
|
19
19
|
# === Queue Observability (v0.1.0) ===
|
|
20
20
|
config.observe_queue = true
|
|
21
21
|
|
|
22
|
-
# === Cache Observability (
|
|
22
|
+
# === Cache Observability (v0.4.0) ===
|
|
23
|
+
# Enable SolidCache event capture and operational clear/prune controls
|
|
23
24
|
# config.observe_cache = true
|
|
24
25
|
# config.cache_sampling_rate = 0.1 # Sample 10% of cache operations
|
|
25
26
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "singleton"
|
|
4
|
+
|
|
5
|
+
module SolidObserver
|
|
6
|
+
class CacheEventBuffer
|
|
7
|
+
include Singleton
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@mutex = Mutex.new
|
|
11
|
+
@buffer = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def push(event_data)
|
|
15
|
+
config = SolidObserver.config
|
|
16
|
+
return unless config.persistence_mode?
|
|
17
|
+
|
|
18
|
+
should_flush = buffer_ready_to_flush?(event_data, config.buffer_size)
|
|
19
|
+
flush! if should_flush
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def flush!
|
|
23
|
+
events = @mutex.synchronize do
|
|
24
|
+
buffered_events = @buffer.dup
|
|
25
|
+
@buffer.clear
|
|
26
|
+
buffered_events
|
|
27
|
+
end
|
|
28
|
+
return if events.empty?
|
|
29
|
+
|
|
30
|
+
Services::FlushCacheEventBuffer.call(events)
|
|
31
|
+
rescue => error
|
|
32
|
+
@mutex.synchronize { @buffer = events + @buffer }
|
|
33
|
+
Rails.logger&.error("[SolidObserver] Cache buffer flush failed: #{error.message}") if defined?(Rails)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def size
|
|
37
|
+
@mutex.synchronize { @buffer.size }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def clear
|
|
41
|
+
@mutex.synchronize { @buffer.clear }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def buffer_ready_to_flush?(event_data, buffer_size)
|
|
47
|
+
@mutex.synchronize do
|
|
48
|
+
@buffer << event_data
|
|
49
|
+
@buffer.size >= buffer_size
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
class CacheSubscriber
|
|
5
|
+
EVENTS = %w[
|
|
6
|
+
cache_read.active_support
|
|
7
|
+
cache_write.active_support
|
|
8
|
+
cache_delete.active_support
|
|
9
|
+
cache_exist?.active_support
|
|
10
|
+
cache_read_multi.active_support
|
|
11
|
+
cache_write_multi.active_support
|
|
12
|
+
cache_delete_multi.active_support
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def subscribe!
|
|
17
|
+
return unless subscription_allowed?
|
|
18
|
+
|
|
19
|
+
@subscriptions = EVENTS.map { |event_name| subscribe_to(event_name) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def unsubscribe!
|
|
23
|
+
return unless @subscriptions
|
|
24
|
+
|
|
25
|
+
@subscriptions.each { |subscription| ActiveSupport::Notifications.unsubscribe(subscription) }
|
|
26
|
+
@subscriptions = []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def subscribed?
|
|
30
|
+
!!@subscriptions&.any?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def subscription_allowed?
|
|
36
|
+
SolidObserver.config.solid_cache_enabled? && !subscribed? && defined?(ActiveSupport::Notifications)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def subscribe_to(event_name)
|
|
40
|
+
ActiveSupport::Notifications.subscribe(event_name) do |*args|
|
|
41
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
42
|
+
Services::RecordCacheEvent.call(event: event, buffer: CacheEventBuffer.instance)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -28,47 +28,50 @@ module SolidObserver
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def gather_storage_stats
|
|
31
|
-
{
|
|
32
|
-
db_size_bytes: SolidObserver::Services::DatabaseSize.call(connection: QueueEvent.connection),
|
|
33
|
-
event_count: QueueEvent.count,
|
|
34
|
-
max_size_bytes: SolidObserver.config.max_db_size
|
|
35
|
-
}
|
|
31
|
+
{components: SolidObserver::Services::StorageInfoSnapshot.call, max_size_bytes: SolidObserver.config.max_db_size}
|
|
36
32
|
rescue => e
|
|
37
33
|
{error: "Failed to gather storage stats: #{e.message}"}
|
|
38
34
|
end
|
|
39
35
|
|
|
40
36
|
def print_storage_table(stats)
|
|
41
|
-
event_count = stats[:event_count]
|
|
42
|
-
db_size_bytes = stats[:db_size_bytes]
|
|
43
37
|
max_size_bytes = stats[:max_size_bytes]
|
|
38
|
+
components = stats[:components]
|
|
44
39
|
|
|
45
40
|
table(
|
|
46
41
|
headers: ["Component", "Size", "Events", "Usage", "Status"],
|
|
47
|
-
rows:
|
|
42
|
+
rows: components.map { |component| storage_row(component: component, max_size_bytes: max_size_bytes) }
|
|
48
43
|
)
|
|
49
44
|
|
|
50
45
|
output("")
|
|
51
46
|
end
|
|
52
47
|
|
|
53
|
-
def storage_row(
|
|
54
|
-
|
|
48
|
+
def storage_row(component:, max_size_bytes:)
|
|
49
|
+
event_count = component[:event_count]
|
|
50
|
+
size, usage, status = storage_displays(component: component, max_size_bytes: max_size_bytes)
|
|
55
51
|
|
|
56
52
|
[
|
|
57
|
-
|
|
53
|
+
component[:label],
|
|
58
54
|
size,
|
|
59
|
-
format_number(event_count),
|
|
55
|
+
event_count ? format_number(event_count) : "—",
|
|
60
56
|
usage,
|
|
61
57
|
status
|
|
62
58
|
]
|
|
63
59
|
end
|
|
64
60
|
|
|
65
|
-
def storage_displays(
|
|
61
|
+
def storage_displays(component:, max_size_bytes:)
|
|
62
|
+
return unavailable_displays unless component[:available]
|
|
63
|
+
|
|
64
|
+
db_size_bytes = component[:db_size_bytes]
|
|
66
65
|
return ["N/A", "N/A", "— Unknown"] unless db_size_bytes
|
|
67
66
|
|
|
68
67
|
percentage = calculate_percentage(db_size_bytes, max_size_bytes)
|
|
69
68
|
[format_size(bytes_to_mb(db_size_bytes)), "#{percentage}%", status_indicator(percentage)]
|
|
70
69
|
end
|
|
71
70
|
|
|
71
|
+
def unavailable_displays
|
|
72
|
+
["—", "—", "— Unavailable"]
|
|
73
|
+
end
|
|
74
|
+
|
|
72
75
|
def print_configuration
|
|
73
76
|
retention_days = (SolidObserver.config.event_retention / 1.day).to_i
|
|
74
77
|
max_size_mb = bytes_to_mb(SolidObserver.config.max_db_size)
|
|
@@ -25,7 +25,9 @@ module SolidObserver
|
|
|
25
25
|
# @note Cache and Cable observers are not yet implemented
|
|
26
26
|
attr_accessor :observe_cache,
|
|
27
27
|
:observe_cable,
|
|
28
|
-
:cache_sampling_rate
|
|
28
|
+
:cache_sampling_rate,
|
|
29
|
+
:cache_slow_threshold,
|
|
30
|
+
:cache_store_errors
|
|
29
31
|
|
|
30
32
|
# Retention Settings
|
|
31
33
|
attr_accessor :event_retention
|
|
@@ -56,12 +58,13 @@ module SolidObserver
|
|
|
56
58
|
@ui_enabled, @ui_base_controller, @ui_username, @ui_password,
|
|
57
59
|
@storage_mode, @observe_queue, @observe_cache, @observe_cable,
|
|
58
60
|
@event_retention, @metrics_retention, @max_db_size, @warning_threshold,
|
|
59
|
-
@sampling_rate, @cache_sampling_rate, @
|
|
61
|
+
@sampling_rate, @cache_sampling_rate, @cache_slow_threshold, @cache_store_errors,
|
|
62
|
+
@buffer_size, @flush_interval,
|
|
60
63
|
@max_buffer_size, @buffer_overflow_strategy, @filter_cache_ttl,
|
|
61
64
|
@correlation_id_generator = !production?, "::ApplicationController", nil, nil,
|
|
62
65
|
:persistence, true, false, false,
|
|
63
66
|
30.days, 90.days, 1.gigabyte, 0.8,
|
|
64
|
-
1.0, 0.1, 1000, 10.seconds,
|
|
67
|
+
1.0, 0.1, 0.1, true, 1000, 10.seconds,
|
|
65
68
|
10_000, :drop_old, 1.minute,
|
|
66
69
|
nil
|
|
67
70
|
end
|
|
@@ -84,6 +87,22 @@ module SolidObserver
|
|
|
84
87
|
@storage_mode == :realtime
|
|
85
88
|
end
|
|
86
89
|
|
|
90
|
+
def solid_queue_available?
|
|
91
|
+
!!defined?(::SolidQueue)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def solid_cache_available?
|
|
95
|
+
!!defined?(::SolidCache)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def solid_queue_enabled?
|
|
99
|
+
observe_queue
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def solid_cache_enabled?
|
|
103
|
+
observe_cache && solid_cache_available?
|
|
104
|
+
end
|
|
105
|
+
|
|
87
106
|
def sampling_rate=(value)
|
|
88
107
|
validate_rate!(:sampling_rate, value)
|
|
89
108
|
@sampling_rate = value
|