solid_observer 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +195 -82
- 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 +52 -0
- data/app/controllers/solid_observer/cache_operations_controller.rb +24 -0
- data/app/controllers/solid_observer/dashboard_controller.rb +38 -1
- data/app/controllers/solid_observer/storages_controller.rb +1 -1
- data/app/helpers/solid_observer/application_helper.rb +268 -5
- data/app/helpers/solid_observer/dashboard_helper.rb +30 -11
- 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_event.rb +15 -0
- data/app/models/solid_observer/cache_metric.rb +13 -0
- data/app/models/solid_observer/storage_info.rb +4 -1
- data/app/views/layouts/solid_observer/application.html.erb +157 -19
- data/app/views/solid_observer/cable_dashboard/_charts.html.erb +31 -0
- data/app/views/solid_observer/cable_dashboard/_recent_events.html.erb +34 -0
- data/app/views/solid_observer/cable_dashboard/_summary.html.erb +34 -0
- data/app/views/solid_observer/cable_dashboard/index.html.erb +118 -0
- data/app/views/solid_observer/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/_queue_table.html.erb +1 -0
- data/app/views/solid_observer/dashboard/index.html.erb +32 -5
- data/app/views/solid_observer/events/index.html.erb +1 -0
- data/app/views/solid_observer/jobs/index.html.erb +1 -0
- data/app/views/solid_observer/jobs/show.html.erb +3 -3
- data/app/views/solid_observer/storages/show.html.erb +90 -32
- data/config/routes.rb +7 -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/db/migrate/20260612000001_add_event_type_recorded_at_index_to_cache_events.rb +21 -0
- data/db/migrate/20260619000001_create_solid_observer_cable_events.rb +22 -0
- data/db/migrate/20260619000002_create_solid_observer_cable_metrics.rb +17 -0
- data/lib/generators/solid_observer/install_generator.rb +8 -1
- data/lib/generators/solid_observer/templates/initializer.rb.tt +20 -4
- 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 +28 -0
- data/lib/solid_observer/cache_metric_buffer.rb +229 -0
- data/lib/solid_observer/cache_subscriber.rb +47 -0
- data/lib/solid_observer/chart_buffer.rb +84 -27
- data/lib/solid_observer/cli/storage.rb +16 -13
- data/lib/solid_observer/configuration.rb +67 -5
- data/lib/solid_observer/engine.rb +70 -15
- data/lib/solid_observer/event_buffer_core.rb +218 -0
- data/lib/solid_observer/queue_event_buffer.rb +9 -201
- data/lib/solid_observer/services/cable_operations.rb +74 -0
- data/lib/solid_observer/services/cable_stats.rb +385 -0
- data/lib/solid_observer/services/cache_operations.rb +115 -0
- data/lib/solid_observer/services/cache_stats.rb +346 -0
- data/lib/solid_observer/services/cleanup_storage.rb +98 -47
- data/lib/solid_observer/services/database_size.rb +13 -8
- 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_event_buffer.rb +54 -0
- data/lib/solid_observer/services/flush_cache_metrics.rb +56 -0
- data/lib/solid_observer/services/record_cable_event.rb +114 -0
- data/lib/solid_observer/services/record_cable_metric.rb +73 -0
- data/lib/solid_observer/services/record_cache_event.rb +165 -0
- data/lib/solid_observer/services/record_cache_metric.rb +66 -0
- data/lib/solid_observer/services/storage_info_snapshot.rb +216 -0
- data/lib/solid_observer/version.rb +1 -1
- data/lib/solid_observer.rb +36 -11
- data/lib/tasks/solid_observer.rake +111 -21
- metadata +47 -5
- data/bin/console +0 -11
- data/bin/quality_gate +0 -95
- data/bin/setup +0 -8
|
@@ -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,43 @@
|
|
|
1
1
|
<% content_for :title, "Dashboard" %>
|
|
2
2
|
|
|
3
3
|
<div class="so-dashboard">
|
|
4
|
+
<% queue_enabled = SolidObserver.config.solid_queue_enabled? %>
|
|
5
|
+
<% queue_status_class = queue_enabled ? "so-badge--success" : "so-badge--warning" %>
|
|
6
|
+
|
|
7
|
+
<div class="so-content__header">
|
|
8
|
+
<h1>Queue overview</h1>
|
|
9
|
+
<div class="so-queue-overview__intro">
|
|
10
|
+
<span class="so-badge so-badge--pill <%= queue_status_class %>">
|
|
11
|
+
<svg class="so-badge__dot" viewBox="0 0 6 6" aria-hidden="true">
|
|
12
|
+
<circle r="3" cx="3" cy="3" />
|
|
13
|
+
</svg>
|
|
14
|
+
<%= queue_enabled ? "Available" : "Unavailable" %>
|
|
15
|
+
</span>
|
|
16
|
+
<% if queue_enabled %>
|
|
17
|
+
<p class="so-queue-overview__hint">Selected range: <%= range_label(@range) %></p>
|
|
18
|
+
<% end %>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<% if !queue_enabled %>
|
|
23
|
+
<section class="so-card so-card--section" aria-labelledby="so-queue-unavailable-heading">
|
|
24
|
+
<h2 id="so-queue-unavailable-heading">Queue Unavailable</h2>
|
|
25
|
+
<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>
|
|
26
|
+
</section>
|
|
27
|
+
<% else %>
|
|
4
28
|
<%# Zone A: Toolbar — range selector, Refresh, Live toggle, help disclosure %>
|
|
5
|
-
<div class="so-dashboard-toolbar" data-so-live>
|
|
6
|
-
<
|
|
29
|
+
<div class="so-dashboard-toolbar" data-so-live data-so-poll-url="<%= poll_data_path %>">
|
|
30
|
+
<form action="<%= request.path %>" method="get" class="so-dashboard-toolbar__left">
|
|
7
31
|
<label for="so-range" class="so-card__label so-filters__label">Range</label>
|
|
8
|
-
<select id="so-range" name="range" data-so-range-select>
|
|
32
|
+
<select id="so-range" name="range" data-so-range-select onchange="this.form.submit()">
|
|
9
33
|
<% SolidObserver::QueueStats::RANGES.each_key do |key| %>
|
|
10
34
|
<option value="<%= key %>" <%= "selected" if key == @range %>><%= key %></option>
|
|
11
35
|
<% end %>
|
|
12
36
|
</select>
|
|
13
37
|
<button type="button" class="so-btn so-btn--refresh" data-so-refresh aria-busy="false">Refresh data</button>
|
|
14
38
|
<span class="so-toolbar-freshness" data-so-freshness aria-live="polite"></span>
|
|
15
|
-
|
|
39
|
+
<input type="hidden" name="live" value="<%= @live ? 'on' : '' %>">
|
|
40
|
+
</form>
|
|
16
41
|
<div class="so-dashboard-toolbar__right">
|
|
17
42
|
<label class="so-toggle so-toggle--pill <%= "so-toggle--on" if @live %>">
|
|
18
43
|
<input type="checkbox" name="live" value="on" <%= "checked" if @live %> data-so-live-toggle>
|
|
@@ -83,6 +108,7 @@
|
|
|
83
108
|
<div class="so-card">
|
|
84
109
|
<div class="so-card__label">Recent Events</div>
|
|
85
110
|
<table class="so-table so-table--card">
|
|
111
|
+
<caption class="sr-only">Recent queue events</caption>
|
|
86
112
|
<thead>
|
|
87
113
|
<tr>
|
|
88
114
|
<th>Event</th>
|
|
@@ -110,4 +136,5 @@
|
|
|
110
136
|
</div>
|
|
111
137
|
<% end %>
|
|
112
138
|
<% end %>
|
|
113
|
-
|
|
139
|
+
<% end %>
|
|
140
|
+
</div>
|
|
@@ -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,97 @@
|
|
|
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
|
+
<% 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 %>
|
|
49
|
+
<% else %>
|
|
50
|
+
<%= component[:unavailable_reason] %>
|
|
51
|
+
<% end %>
|
|
52
|
+
</div>
|
|
53
|
+
</article>
|
|
54
|
+
<% end %>
|
|
55
|
+
</div>
|
|
56
|
+
</section>
|
|
57
|
+
|
|
58
|
+
<% if @storage_history.any? %>
|
|
59
|
+
<div class="so-card so-card--section">
|
|
60
|
+
<div class="so-card__label">Recent Snapshots</div>
|
|
61
|
+
<table class="so-table so-table--card">
|
|
62
|
+
<caption class="sr-only">Recent storage snapshots by component</caption>
|
|
63
|
+
<thead>
|
|
27
64
|
<tr>
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
<
|
|
65
|
+
<th>Time</th>
|
|
66
|
+
<th>Component</th>
|
|
67
|
+
<th>Size</th>
|
|
68
|
+
<th>Records</th>
|
|
31
69
|
</tr>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
70
|
+
</thead>
|
|
71
|
+
<tbody>
|
|
72
|
+
<% @storage_history.each do |snapshot| %>
|
|
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 %>
|
|
83
|
+
<tr>
|
|
84
|
+
<td><%= snapshot.recorded_at.strftime("%Y-%m-%d %H:%M") %></td>
|
|
85
|
+
<td><%= component_label %></td>
|
|
86
|
+
<td><%= number_to_human_size(snapshot.db_size_bytes, precision: 1, significant: false, strip_insignificant_zeros: false) %></td>
|
|
87
|
+
<td><%= number_with_delimiter(snapshot.event_count) %> <%= record_label %></td>
|
|
88
|
+
</tr>
|
|
89
|
+
<% end %>
|
|
90
|
+
</tbody>
|
|
91
|
+
</table>
|
|
92
|
+
</div>
|
|
93
|
+
<% end %>
|
|
94
|
+
<% else %>
|
|
95
|
+
<%= render "solid_observer/shared/empty_state", message: "No storage information available" %>
|
|
36
96
|
<% end %>
|
|
37
|
-
|
|
38
|
-
<%= render "solid_observer/shared/empty_state", message: "No storage information available" %>
|
|
39
|
-
<% end %>
|
|
97
|
+
</div>
|
data/config/routes.rb
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
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 "cable", to: "cable_dashboard#index", as: :cable_dashboard
|
|
8
|
+
post "cable/trim", to: "cable_operations#trim", as: :trim_cable_operations
|
|
9
|
+
get "cache/controls", to: "cache_operations#index", as: :cache_operations
|
|
10
|
+
post "cache/controls/prune", to: "cache_operations#prune", as: :prune_cache_operations
|
|
11
|
+
post "cache/controls/clear", to: "cache_operations#clear", as: :clear_cache_operations
|
|
5
12
|
get "poll_data", to: "dashboard#poll_data", as: :poll_data
|
|
6
13
|
get "live_poll.js", to: "dashboard#live_poll", as: :live_poll_script
|
|
7
14
|
|
|
@@ -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
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddEventTypeRecordedAtIndexToCacheEvents < ActiveRecord::Migration[8.0]
|
|
4
|
+
disable_ddl_transaction!
|
|
5
|
+
|
|
6
|
+
def change
|
|
7
|
+
options = concurrent_supported? ? {algorithm: :concurrently} : {}
|
|
8
|
+
|
|
9
|
+
add_index :solid_observer_cache_events,
|
|
10
|
+
%i[event_type recorded_at],
|
|
11
|
+
order: {recorded_at: :desc},
|
|
12
|
+
if_not_exists: true,
|
|
13
|
+
**options
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def concurrent_supported?
|
|
19
|
+
connection.adapter_name.match?(/postgres/i)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateSolidObserverCableEvents < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :solid_observer_cable_events do |t|
|
|
6
|
+
t.string :event_type, null: false, limit: 64
|
|
7
|
+
t.string :channel_class, limit: 255
|
|
8
|
+
t.string :broadcasting_digest, limit: 64
|
|
9
|
+
t.float :duration
|
|
10
|
+
t.string :error_class, limit: 255
|
|
11
|
+
t.text :error_message
|
|
12
|
+
t.text :metadata
|
|
13
|
+
t.datetime :recorded_at, null: false
|
|
14
|
+
|
|
15
|
+
t.index :recorded_at
|
|
16
|
+
t.index :event_type
|
|
17
|
+
t.index :channel_class
|
|
18
|
+
t.index :broadcasting_digest
|
|
19
|
+
t.index :error_class
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateSolidObserverCableMetrics < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :solid_observer_cable_metrics do |t|
|
|
6
|
+
t.datetime :period_start, null: false
|
|
7
|
+
t.bigint :broadcasts_count, null: false, default: 0
|
|
8
|
+
t.bigint :transmissions_count, null: false, default: 0
|
|
9
|
+
t.bigint :confirmations_count, null: false, default: 0
|
|
10
|
+
t.bigint :rejections_count, null: false, default: 0
|
|
11
|
+
t.bigint :perform_actions_count, null: false, default: 0
|
|
12
|
+
t.bigint :errors_count, null: false, default: 0
|
|
13
|
+
|
|
14
|
+
t.index :period_start, unique: true, name: "idx_solid_observer_cable_metrics_unique"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -17,8 +17,11 @@ module SolidObserver
|
|
|
17
17
|
%w[development test production].each do |env|
|
|
18
18
|
config_block = <<-YAML
|
|
19
19
|
solid_observer_queue:
|
|
20
|
-
|
|
20
|
+
adapter: sqlite3
|
|
21
|
+
pool: 5
|
|
22
|
+
timeout: 5000
|
|
21
23
|
database: storage/#{env}_solid_observer_queue.sqlite3
|
|
24
|
+
migrations_paths: db/solid_observer_migrate
|
|
22
25
|
YAML
|
|
23
26
|
inject_into_file "config/database.yml", config_block, after: /^#{env}:\n(?: .*\n)*/
|
|
24
27
|
end
|
|
@@ -40,6 +43,10 @@ module SolidObserver
|
|
|
40
43
|
say " 5. Restart your Rails server"
|
|
41
44
|
say " 6. Visit /solid_observer to access the web dashboard"
|
|
42
45
|
say "\n"
|
|
46
|
+
say "IMPORTANT: If your host app uses PostgreSQL or MySQL, review the", :yellow
|
|
47
|
+
say "solid_observer_queue entries in config/database.yml before running", :yellow
|
|
48
|
+
say "db:create. The generated config uses adapter: sqlite3 by default.", :yellow
|
|
49
|
+
say "\n"
|
|
43
50
|
say "Documentation: https://solid.observer", :red
|
|
44
51
|
say "GitHub: https://github.com/bart-oz/solid_observer", :red
|
|
45
52
|
say "\n"
|
|
@@ -9,22 +9,38 @@ SolidObserver.configure do |config|
|
|
|
9
9
|
# Recommended: false in production, true in development/staging
|
|
10
10
|
config.ui_enabled = !Rails.env.production?
|
|
11
11
|
|
|
12
|
-
#
|
|
12
|
+
# Production dashboard exposure is the host app's responsibility. If you mount
|
|
13
|
+
# the dashboard in production, wrap the mount in your existing admin auth
|
|
14
|
+
# constraint, for example in config/routes.rb:
|
|
15
|
+
#
|
|
16
|
+
# authenticate :user, ->(user) { user.admin? } do
|
|
17
|
+
# mount SolidObserver::Engine, at: "/solid_observer"
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# Built-in HTTP Basic Auth is enabled only when BOTH credentials are present.
|
|
21
|
+
# Setting only one credential disables Basic Auth and logs a boot warning.
|
|
13
22
|
# config.ui_username = "admin"
|
|
14
23
|
# config.ui_password = "secret"
|
|
15
24
|
|
|
16
|
-
# Base controller
|
|
25
|
+
# Base controller used only to detect API-only rendering compatibility
|
|
17
26
|
# config.ui_base_controller = "ApplicationController"
|
|
18
27
|
|
|
19
28
|
# === Queue Observability (v0.1.0) ===
|
|
20
29
|
config.observe_queue = true
|
|
21
30
|
|
|
22
|
-
# === Cache Observability (
|
|
31
|
+
# === Cache Observability (v0.4.0) ===
|
|
32
|
+
# Enable SolidCache event capture and operational clear/prune controls
|
|
23
33
|
# config.observe_cache = true
|
|
24
34
|
# config.cache_sampling_rate = 0.1 # Sample 10% of cache operations
|
|
25
35
|
|
|
26
|
-
# === Cable Observability (
|
|
36
|
+
# === Cable Observability (v0.5.0) ===
|
|
37
|
+
# Enable SolidCable event capture and trim controls.
|
|
38
|
+
# Requires SolidCable in the host app; SolidObserver does not add it.
|
|
27
39
|
# config.observe_cable = true
|
|
40
|
+
# config.cable_sampling_rate = 0.1 # Sample 10% of broadcast events
|
|
41
|
+
# config.cable_rejection_threshold = 0.05 # Rejection rate → Degraded stability
|
|
42
|
+
# config.cable_backlog_threshold = 0.10 # Backlog ratio threshold
|
|
43
|
+
# config.cable_error_threshold = 0.0 # Error rate threshold
|
|
28
44
|
|
|
29
45
|
# Data Retention
|
|
30
46
|
config.event_retention = 30.days # How long to keep event records
|
|
@@ -7,7 +7,7 @@ module SolidObserver
|
|
|
7
7
|
# will be configured by the Engine (similar to BaseEvent) when metrics are
|
|
8
8
|
# fully implemented.
|
|
9
9
|
#
|
|
10
|
-
class BaseMetric <
|
|
10
|
+
class BaseMetric < BaseRecord
|
|
11
11
|
self.abstract_class = true
|
|
12
12
|
self.table_name = "solid_observer_metrics"
|
|
13
13
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "singleton"
|
|
4
|
+
|
|
5
|
+
require_relative "event_buffer_core"
|
|
6
|
+
|
|
7
|
+
module SolidObserver
|
|
8
|
+
class CableEventBuffer
|
|
9
|
+
include Singleton
|
|
10
|
+
include EventBufferCore
|
|
11
|
+
|
|
12
|
+
INITIAL_METRICS = EventBufferCore::INITIAL_METRICS
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
initialize_event_buffer
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def flush_service
|
|
21
|
+
Services::FlushCableEventBuffer
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def log_label
|
|
25
|
+
"Cable buffer"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|