solid_observer 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +80 -20
  4. data/app/assets/javascripts/solid_observer/live_poll.js +3 -1
  5. data/app/controllers/solid_observer/application_controller.rb +1 -0
  6. data/app/controllers/solid_observer/cable_dashboard_controller.rb +52 -0
  7. data/app/controllers/solid_observer/cable_operations_controller.rb +16 -0
  8. data/app/controllers/solid_observer/cache_dashboard_controller.rb +33 -40
  9. data/app/controllers/solid_observer/dashboard_controller.rb +1 -7
  10. data/app/helpers/solid_observer/application_helper.rb +114 -0
  11. data/app/models/solid_observer/cable_event.rb +13 -0
  12. data/app/models/solid_observer/cable_metric.rb +12 -0
  13. data/app/models/solid_observer/cache_metric.rb +1 -2
  14. data/app/models/solid_observer/storage_info.rb +1 -1
  15. data/app/views/layouts/solid_observer/application.html.erb +19 -8
  16. data/app/views/solid_observer/cable_dashboard/_charts.html.erb +31 -0
  17. data/app/views/solid_observer/cable_dashboard/_recent_events.html.erb +34 -0
  18. data/app/views/solid_observer/cable_dashboard/_summary.html.erb +34 -0
  19. data/app/views/solid_observer/cable_dashboard/index.html.erb +118 -0
  20. data/app/views/solid_observer/dashboard/_queue_table.html.erb +1 -0
  21. data/app/views/solid_observer/dashboard/index.html.erb +2 -5
  22. data/app/views/solid_observer/events/index.html.erb +1 -0
  23. data/app/views/solid_observer/jobs/index.html.erb +1 -0
  24. data/app/views/solid_observer/storages/show.html.erb +29 -3
  25. data/config/routes.rb +2 -0
  26. data/db/migrate/20260612000001_add_event_type_recorded_at_index_to_cache_events.rb +21 -0
  27. data/db/migrate/20260619000001_create_solid_observer_cable_events.rb +22 -0
  28. data/db/migrate/20260619000002_create_solid_observer_cable_metrics.rb +17 -0
  29. data/lib/generators/solid_observer/install_generator.rb +8 -1
  30. data/lib/generators/solid_observer/templates/initializer.rb.tt +18 -3
  31. data/lib/solid_observer/base_event.rb +1 -1
  32. data/lib/solid_observer/base_metric.rb +1 -1
  33. data/lib/solid_observer/base_record.rb +8 -0
  34. data/lib/solid_observer/cable_event_buffer.rb +28 -0
  35. data/lib/solid_observer/cable_metric_buffer.rb +230 -0
  36. data/lib/solid_observer/cable_subscriber.rb +57 -0
  37. data/lib/solid_observer/cache_event_buffer.rb +11 -36
  38. data/lib/solid_observer/cache_metric_buffer.rb +229 -0
  39. data/lib/solid_observer/chart_buffer.rb +84 -27
  40. data/lib/solid_observer/configuration.rb +47 -4
  41. data/lib/solid_observer/engine.rb +46 -28
  42. data/lib/solid_observer/event_buffer_core.rb +218 -0
  43. data/lib/solid_observer/queue_event_buffer.rb +9 -201
  44. data/lib/solid_observer/services/cable_operations.rb +74 -0
  45. data/lib/solid_observer/services/cable_stats.rb +385 -0
  46. data/lib/solid_observer/services/cache_stats.rb +35 -18
  47. data/lib/solid_observer/services/cleanup_storage.rb +82 -47
  48. data/lib/solid_observer/services/flush_cable_event_buffer.rb +54 -0
  49. data/lib/solid_observer/services/flush_cable_metrics.rb +54 -0
  50. data/lib/solid_observer/services/flush_cache_metrics.rb +56 -0
  51. data/lib/solid_observer/services/record_cable_event.rb +114 -0
  52. data/lib/solid_observer/services/record_cable_metric.rb +73 -0
  53. data/lib/solid_observer/services/record_cache_event.rb +23 -0
  54. data/lib/solid_observer/services/record_cache_metric.rb +13 -21
  55. data/lib/solid_observer/services/storage_info_snapshot.rb +103 -15
  56. data/lib/solid_observer/version.rb +1 -1
  57. data/lib/solid_observer.rb +36 -11
  58. data/lib/tasks/solid_observer.rake +84 -23
  59. metadata +26 -6
  60. data/app/assets/stylesheets/solid_observer/application.css +0 -18
  61. data/bin/console +0 -11
  62. data/bin/quality_gate +0 -95
  63. data/bin/setup +0 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9c3e774c9276371b59f696226979b922772191e0531a4d9ba2dbb09b8e3e452
4
- data.tar.gz: 570ae5aacef981358f68998d3df14bfc532e2af48cbdd21c6cd18ecbfa7132e7
3
+ metadata.gz: 563c4874ef7e6a8ad8a485d0b3f86c9d1d8f924d5308d44c5137f76bda7e03de
4
+ data.tar.gz: 88ba3331b45977349604047db1a4e3e98efb17ebc1eca8d4c52a96825d320a83
5
5
  SHA512:
6
- metadata.gz: 50684bd0b286fc9b93f77a57f5d0b60c3e0334b6d7804bf63307b8cdc05a78cab5bb586305b96617f5fe0e76f76cd89112c6cb6e059a45d190ca6dc7c49fa885
7
- data.tar.gz: 9e456400209630d4e3b777355bf1d1a651e0bac1d90d6d40de4272c9cce8afb322bf5ff9afed10fe1a9e0911c7e84c15d02e6b2de3360e04f5d4d349bbf02f34
6
+ metadata.gz: e56550b5f3cfbbb6a54325901a19bb4a70850a994e047d282cea653b420822cd1a9148c603c2886c92fb5bc9683b650587db575ef90d85b39800430ee7ffbdde
7
+ data.tar.gz: dbf38ea6c5205ef027642cf02b84e3c59950299ea0477a2f7fef8df7543d71ece58dd90026f396650a99ad948ffa49cf16978f6a596c998d87ab085df739a114
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## [0.5.0] - 2026-06-24
2
+
3
+ Headline: **Solid Cable observability** — optional Cable telemetry, dashboard, storage health, and guarded trim controls.
4
+
5
+ ### Added
6
+ - **Cable telemetry foundation** (SO-089, SO-090) — `observe_cable` config gate with `solid_cable_available?`/`solid_cable_enabled?` predicates; `CableSubscriber` hooks into `ActiveSupport::Notifications` broadcast events; event/metric buffers reuse `EventBufferCore`; `CableEvent`/`CableMetric` models with two migrations; `cable_sampling_rate` default 0.1; broadcasting names stored as `Digest::SHA256.hexdigest` (PII boundary).
7
+ - **Cable storage health** (SO-091) — Storages page aggregates Solid Cable message storage and SolidObserver Cable telemetry rows with optional fallback states.
8
+ - **Cable dashboard** (SO-092) — `/solid_observer/cable_dashboard` with summary cards, broadcast/rejection trends, stability indicator (hybrid: event + backlog signals), recent events. Three configurable thresholds: `cable_rejection_threshold` (0.05), `cable_backlog_threshold` (0.10), `cable_error_threshold` (0.0). Sidebar navigation entry.
9
+ - **Guarded Cable trim** (SO-093) — UI button for ≤1,000 trimmable messages; `solid_observer:cable:trim` Rake task for larger backlogs. No clear-all.
10
+
11
+ ### Changed
12
+ - **Self-contained SQLite generator config** (SO-094) — install generator produces a `solid_observer_queue` block with explicit `adapter: sqlite3`, `pool`, `timeout`, `database`, and `migrations_paths: db/solid_observer_migrate` — no `<<: *default` YAML merge. Post-install warning printed for PostgreSQL/MySQL hosts. Cable telemetry label standardised to `Cable telemetry`.
13
+
1
14
  ## [0.4.0] - 2026-06-03
2
15
 
3
16
  Headline: **Solid Cache observability** — multi-component foundation, cache dashboard, operational controls, and shared storage health.
data/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  </p>
8
8
 
9
9
  <p align="center">
10
- <a href="https://github.com/bart-oz/solid_observer/releases"><img src="https://img.shields.io/badge/version-0.4.0-blue.svg" alt="Version"></a>
10
+ <a href="https://github.com/bart-oz/solid_observer/releases"><img src="https://img.shields.io/badge/version-0.5.0-blue.svg" alt="Version"></a>
11
11
  <a href="LICENSE.txt"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
12
12
  <a href="https://github.com/bart-oz/solid_observer/actions"><img src="https://img.shields.io/badge/tests-passing-brightgreen.svg" alt="Tests"></a>
13
13
  <a href="https://github.com/bart-oz/solid_observer/actions"><img src="https://img.shields.io/badge/coverage-96.22%25-brightgreen.svg" alt="Coverage"></a>
@@ -18,20 +18,20 @@
18
18
  <a href=".github/assets/dash_1.png"><img src=".github/assets/dash_1.png" alt="SolidObserver dashboard overview" width="700"></a>
19
19
  </p>
20
20
 
21
- SolidObserver is a production-grade observability solution for Rails 8's Solid Stack. v0.4.0 covers both **Solid Queue** and **Solid Cache** with a unified Web UI dashboard, CLI tools, metrics collection, and distributed tracing support.
21
+ SolidObserver is a production-grade observability solution for Rails 8's Solid Stack. v0.5.0 covers **Solid Queue**, **Solid Cache**, and **Solid Cable** with a unified Web UI dashboard, CLI tools, metrics collection, and distributed tracing support.
22
22
 
23
- ## Features (v0.4.0)
23
+ ## Features (v0.5.0)
24
24
 
25
- | | Solid Queue | Solid Cache |
26
- |---|---|---|
27
- | **Web UI Dashboard** | Queue stats, jobs browser, events log | Hit rate, ops/sec, error rate, avg duration |
28
- | **Storage footprint** | DB size, event counts | SolidCache table size, row counts |
29
- | **Activity trends** | Sparklines (Performed, Ready, Failed) | Activity trend sparklines |
30
- | **Stability indicator** | Stable / Degraded / Critical badge | Stability pill badge |
31
- | **Operational controls** | Retry / discard failed jobs | Prune expired entries, clear all entries |
32
- | **CLI tools** | status, jobs:list/show/retry/discard | cache:prune, cache:clear |
33
- | **Privacy** | Job arguments excluded from persisted events | Keys and values **never** shown |
34
- | **Operating modes** | Real-time (no DB) or persistence (full history) | Persistence mode only |
25
+ | | Solid Queue | Solid Cache | Solid Cable |
26
+ |---|---|---|---|
27
+ | **Web UI Dashboard** | Queue stats, jobs browser, events log | Hit rate, ops/sec, error rate, avg duration | Broadcasts, rejection rate, trends |
28
+ | **Storage footprint** | DB size, event counts | SolidCache table size, row counts | Message count + backlogs |
29
+ | **Activity trends** | Sparklines (Performed, Ready, Failed) | Activity trend sparklines | Broadcast/rejection sparklines |
30
+ | **Stability indicator** | Stable / Degraded / Critical badge | Stability pill badge | Stable / Degraded / Critical (hybrid) |
31
+ | **Operational controls** | Retry / discard failed jobs | Prune expired entries, clear all entries | Trim expired messages |
32
+ | **CLI tools** | status, jobs:list/show/retry/discard | cache:prune, cache:clear | cable:trim |
33
+ | **Privacy** | Job arguments excluded from persisted events | Keys and values **never** shown | Broadcasting names hashed (SHA256) |
34
+ | **Operating modes** | Real-time (no DB) or persistence (full history) | Persistence mode only | Persistence mode only |
35
35
 
36
36
  Additional: 🔗 APM distributed tracing · ⚡ buffered writes · 🛡️ Docker/CI/K8s safe boot
37
37
 
@@ -40,6 +40,8 @@ Additional: 🔗 APM distributed tracing · ⚡ buffered writes · 🛡️ Docke
40
40
  - Ruby 3.2+
41
41
  - Rails 8.0+
42
42
  - Solid Queue (configured with `connects_to` in all environments)
43
+ - Solid Cache (optional — enable with `config.observe_cache = true`)
44
+ - Solid Cable (optional — enable with `config.observe_cable = true`)
43
45
 
44
46
  > **Note:** Ensure Solid Queue is configured with `connects_to` in all environments, not just production. See [Troubleshooting](#troubleshooting) if you encounter database connection issues.
45
47
 
@@ -75,7 +77,7 @@ That's it. You now have access to queue status, job listing, retry, and discard
75
77
 
76
78
  Store event history, metrics, and storage snapshots in a dedicated observer database. This gives you everything in real-time mode plus long-term event tracking, buffered writes, and retention-based cleanup. The install generator defaults to SQLite; the database can use any Rails-supported adapter for record persistence, and storage-size monitoring is implemented for SQLite, PostgreSQL/PostGIS, MySQL, and Trilogy. See [Database Setup](#database-setup-persistence-mode) below.
77
79
 
78
- > If your host app uses a different adapter than SQLite (e.g. PostgreSQL or MySQL), see [Multi-adapter setup](#multi-adapter-setup) before running these commands.
80
+ > **⚠️ PostgreSQL / MySQL hosts:** The generator writes a self-contained `solid_observer_queue` block with `adapter: sqlite3`. **Review `config/database.yml` before running `db:create`** — do not merge `<<: *default` into the observer entry, as that pulls the host adapter in and `db:create` will fail. See [Multi-adapter setup](#multi-adapter-setup) for the correct pattern.
79
81
 
80
82
  ```bash
81
83
  bin/rails solid_observer:install:migrations
@@ -182,6 +184,18 @@ The install generator mounts it for you. To mount manually:
182
184
  mount SolidObserver::Engine, at: "/solid_observer"
183
185
  ```
184
186
 
187
+ Production dashboard exposure is the host application's responsibility. If you
188
+ enable and mount the dashboard in production, wrap the mount in your existing
189
+ admin authentication/authorization constraint (example shown for apps that
190
+ already provide an `authenticate` route helper):
191
+
192
+ ```ruby
193
+ # config/routes.rb
194
+ authenticate :user, ->(user) { user.admin? } do
195
+ mount SolidObserver::Engine, at: "/solid_observer"
196
+ end
197
+ ```
198
+
185
199
  ### Auth Configuration
186
200
 
187
201
  ```ruby
@@ -207,6 +221,7 @@ For production hardening (fail-loud on missing env var vs. fail-open), see [Conf
207
221
  - **API-only apps** — the Web UI works without manual configuration. The engine ships its own Cookies/Session/Flash middleware stack scoped to `/solid_observer/*`.
208
222
  - **Host-app callbacks are not inherited.** Use `ui_username` / `ui_password` for built-in HTTP Basic Auth.
209
223
  - **Auth misconfiguration is fail-open, but loud.** If `ui_username` is set but `ui_password` resolves to `nil`, the UI ships unauthenticated and logs a boot `WARNING`.
224
+ - **HTTP Basic Auth requires both credentials.** Setting only `ui_username` or only `ui_password` disables built-in auth; protect production mounts at the host-app route boundary.
210
225
 
211
226
  See the full component breakdown in [Components](#components) below.
212
227
 
@@ -270,16 +285,51 @@ bin/rails solid_observer:cache:clear # Clear all SolidCache entries (with confi
270
285
  bin/rails solid_observer:cache:prune # Prune expired SolidCache entries
271
286
  ```
272
287
 
288
+ ### Solid Cable Observability
289
+
290
+ Cable observability is optional. Enable with `config.observe_cable = true`. Requires SolidCable in the host app. SolidObserver does not add any SolidCable dependency.
291
+
292
+ Cable dashboard (`/solid_observer/cable_dashboard`) shows broadcast/rejection trends, a stability indicator (hybrid: event + backlog signals), and recent safe events. Storages page includes Cable telemetry rows. Guarded trim controls: UI button for ≤1,000 trimmable messages; `solid_observer:cable:trim` Rake task for larger backlogs. **Broadcasting names are stored as SHA256 digests** — raw names are never persisted.
293
+
294
+ **Enabling Cable observability:**
295
+
296
+ ```ruby
297
+ SolidObserver.configure do |config|
298
+ config.observe_cable = true # default: false
299
+ # config.cable_sampling_rate = 0.1 # Sample 10% of broadcast events (default: 0.1)
300
+ # config.cable_rejection_threshold = 0.05 # Rejection rate threshold for Degraded (default: 0.05)
301
+ # config.cable_backlog_threshold = 0.10 # Backlog ratio threshold (default: 0.10)
302
+ # config.cable_error_threshold = 0.0 # Error rate threshold (default: 0.0)
303
+ end
304
+ ```
305
+
306
+ `solid_cable_enabled?` is `true` when both `observe_cable = true` and SolidCable is available in the host app.
307
+
308
+ **CLI commands (Solid Cable):**
309
+
310
+ ```bash
311
+ bin/rails solid_observer:cable:trim # Trim expired Cable messages (with confirmation)
312
+ ```
313
+
314
+ > Cable dashboard screenshots are not yet available — capture from a host app with SolidCable configured.
315
+
273
316
  ### Storage
274
317
 
275
- The Storage page aggregates per-component health rows: Queue Observer database, Cache Observer, and SolidCache table sizes. Each row reports size, record counts, and a status indicator.
318
+ The Storage page aggregates per-component health rows: Queue Observer database, Cache Observer, SolidCache table sizes, and Cable telemetry. Each row reports size, record counts, and a status indicator.
276
319
 
277
320
  <p align="center">
278
- <a href=".github/assets/dash_8.png"><img src=".github/assets/dash_8.png" alt="Component health — Queue Observer + Cache Observer + SolidCache" width="700"></a>
321
+ <a href=".github/assets/dash_8.png"><img src=".github/assets/dash_8.png" alt="Component health — Queue Observer + Cache Observer + SolidCache + Cable telemetry" width="700"></a>
279
322
  </p>
280
323
 
281
324
  For adapter notes and multi-adapter setup, see [Database Setup](#database-setup-persistence-mode) below.
282
325
 
326
+ ### Storage unavailable diagnostics
327
+
328
+ The dashboard is admin/developer-facing. When SolidObserver storage is not
329
+ reachable, the current default 503 error page includes the raw exception class
330
+ and message so maintainers can diagnose adapter, credential, migration, or
331
+ database availability problems. Do not expose the dashboard to untrusted users.
332
+
283
333
  ## Configuration
284
334
 
285
335
  <details>
@@ -300,6 +350,13 @@ SolidObserver.configure do |config|
300
350
  # Enable cache monitoring (default: false; requires SolidCache in host app)
301
351
  config.observe_cache = true
302
352
 
353
+ # Enable cable monitoring (default: false; requires SolidCable in host app)
354
+ config.observe_cable = true
355
+ config.cable_sampling_rate = 0.1 # Sample 10% of broadcast events
356
+ config.cable_rejection_threshold = 0.05 # Rejection rate → Degraded stability
357
+ config.cable_backlog_threshold = 0.10 # Backlog ratio threshold
358
+ config.cable_error_threshold = 0.0 # Error rate threshold
359
+
303
360
  # Data Retention (persistence mode only)
304
361
  config.event_retention = 30.days # Keep events for 30 days
305
362
  config.metrics_retention = 90.days # Keep metrics for 90 days
@@ -386,6 +443,7 @@ end
386
443
  | `solid_observer:storage:purge` | Delete ALL SolidObserver data |
387
444
  | `solid_observer:cache:clear` | Clear all SolidCache entries (with confirmation) |
388
445
  | `solid_observer:cache:prune` | Prune expired SolidCache entries |
446
+ | `solid_observer:cable:trim` | Trim expired Solid Cable messages |
389
447
 
390
448
  > **Note:** Storage commands manage **SolidObserver's storage** (event logs, metrics, snapshots) — not Solid Queue's jobs. Cache commands operate on SolidCache entries in the host app's cache store.
391
449
 
@@ -423,16 +481,18 @@ The example below is what `rails generate solid_observer:install` produces:
423
481
  ```yaml
424
482
  # config/database.yml (generator default)
425
483
  solid_observer_queue:
426
- <<: *default
427
484
  adapter: sqlite3
485
+ pool: 5
486
+ timeout: 5000
428
487
  database: storage/<%= Rails.env %>_solid_observer_queue.sqlite3
488
+ migrations_paths: db/solid_observer_migrate
429
489
  ```
430
490
 
431
491
  > **Note:** SQLite is the generator default, not a requirement. The `solid_observer_queue` database can use any Rails-supported adapter for record persistence. Adapter-native **storage-size monitoring** is currently implemented for SQLite, PostgreSQL/PostGIS, MySQL, and Trilogy. On other adapters, the size query returns `nil` and the engine logs a single `[SolidObserver] Unknown adapter for DatabaseSize: …` warning — record persistence still works.
432
492
 
433
493
  ### Multi-adapter setup
434
494
 
435
- If your host application uses PostgreSQL and you want SolidObserver to use SQLite, keep the `solid_observer_queue` block **self-contained** — do not rely on `<<: *default`. The generator default uses `<<: *default` with an explicit `adapter: sqlite3` override; that is safe on SQLite-primary hosts. On a PostgreSQL host, merging `<<: *default` without an explicit adapter override pulls the PG adapter in and fails at `db:create`. For multi-adapter connections, omit the merge entirely:
495
+ If your host application uses PostgreSQL and you want SolidObserver to use SQLite, keep the `solid_observer_queue` block **self-contained** — do not rely on `<<: *default`. The generator produces a self-contained block with explicit `adapter: sqlite3` and `migrations_paths`, which is safe on any host adapter. On a PostgreSQL host, merging `<<: *default` without an explicit adapter override pulls the PG adapter in and fails at `db:create`. For multi-adapter connections, omit the merge entirely:
436
496
 
437
497
  ```yaml
438
498
  # config/database.yml
@@ -473,8 +533,8 @@ gem "sqlite3", "~> 2.0"
473
533
  | v0.1.0 | Solid Queue monitoring, CLI tools | ✅ Released |
474
534
  | v0.1.1 | Real-time mode (no migrations needed) | ✅ Released |
475
535
  | v0.3.0 | Web UI dashboard + stability hardening | ✅ Released |
476
- | v0.4.0 | Solid Cache monitoring | ✅ Current |
477
- | v0.5.0 | Solid Cable monitoring | 🔜 Planned |
536
+ | v0.4.0 | Solid Cache monitoring | ✅ Released |
537
+ | v0.5.0 | Solid Cable monitoring | Current |
478
538
  | v0.6.0 | Cross-component correlation, health scores | 🔜 Planned |
479
539
  | v0.7.0 | Alerting & notifications | 🔜 Planned |
480
540
  | v1.0.0 | Production stable release | 🎯 Goal |
@@ -10,7 +10,7 @@
10
10
  var checkbox, rangeSelect, refreshBtn, helpBtn, helpPanel, helpWrapper, freshnessEl;
11
11
  var hoverActive = false;
12
12
  var sparks = {};
13
- var url = "/solid_observer/poll_data";
13
+ var url;
14
14
  var inFlight = false;
15
15
  var timerId = null;
16
16
  var lastFullSnapshot = null;
@@ -21,6 +21,8 @@
21
21
  var wrapper = document.querySelector("[data-so-live]");
22
22
  if (!wrapper) return;
23
23
 
24
+ url = wrapper.getAttribute("data-so-poll-url") || "/solid_observer/poll_data";
25
+
24
26
  checkbox = wrapper.querySelector('[data-so-live-toggle]');
25
27
  if (!checkbox) return;
26
28
 
@@ -6,6 +6,7 @@ module SolidObserver
6
6
  [
7
7
  *([PG::ConnectionBad] if defined?(PG::ConnectionBad)),
8
8
  *([Mysql2::Error::ConnectionError] if defined?(Mysql2::Error::ConnectionError)),
9
+ *([Trilogy::Error] if defined?(Trilogy::Error)),
9
10
  *([SQLite3::CantOpenException] if defined?(SQLite3::CantOpenException))
10
11
  ]
11
12
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dashboard_controller"
4
+
5
+ module SolidObserver
6
+ # :reek:TooManyInstanceVariables
7
+ class CableDashboardController < DashboardController
8
+ CABLE_STORAGE_COMPONENTS = %w[cable_observer solid_cable].freeze
9
+
10
+ def index
11
+ @component = "cable"
12
+ assign_cable_dashboard
13
+ end
14
+
15
+ private
16
+
17
+ # :reek:TooManyStatements
18
+ def assign_cable_dashboard
19
+ unless SolidObserver.config.solid_cable_enabled?
20
+ @cable_dashboard_available = false
21
+ @storage_components = []
22
+ @recent_events = []
23
+ return
24
+ end
25
+
26
+ range = SolidObserver::Services::CableStats.parse_range(request_range_param)
27
+ window = SolidObserver::Services::CableStats.range_duration(range)
28
+ stats = SolidObserver::Services::CableStats.call(window: window)
29
+
30
+ @cable_dashboard_available = true
31
+ @range = range
32
+ @stats = stats
33
+ @storage_components = cable_storage_components
34
+ @recent_events = recent_events(window)
35
+ end
36
+
37
+ def cable_storage_components
38
+ SolidObserver::Services::StorageInfoSnapshot.call.select do |snapshot|
39
+ CABLE_STORAGE_COMPONENTS.include?(snapshot[:component])
40
+ end
41
+ rescue *SolidObserver::Services::StorageInfoSnapshot::CONNECTION_ERRORS, TypeError, NoMethodError
42
+ []
43
+ end
44
+
45
+ def recent_events(window)
46
+ current_time = Time.current
47
+ SolidObserver::CableEvent.where(recorded_at: (current_time - window)..current_time).recent(10)
48
+ rescue ActiveRecord::StatementInvalid
49
+ []
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidObserver
4
+ class CableOperationsController < ApplicationController
5
+ def trim
6
+ redirect_with_result(SolidObserver::Services::CableOperations.trim)
7
+ end
8
+
9
+ private
10
+
11
+ def redirect_with_result(result)
12
+ flash_key = result[:ok] ? :notice : :alert
13
+ redirect_to cable_dashboard_path, flash_key => result[:message]
14
+ end
15
+ end
16
+ end
@@ -6,54 +6,47 @@ module SolidObserver
6
6
  class CacheDashboardController < DashboardController
7
7
  CACHE_STORAGE_COMPONENTS = %w[solid_cache cache_observer].freeze
8
8
 
9
- class << self
10
- def cache_dashboard_assignments(range_param:)
11
- return unavailable_assignments unless SolidObserver.config.solid_cache_enabled?
12
-
13
- range = SolidObserver::Services::CacheStats.parse_range(range_param)
14
- window = SolidObserver::Services::CacheStats.range_duration(range)
15
- stats = SolidObserver::Services::CacheStats.call(window: window)
16
-
17
- {
18
- cache_dashboard_available: true,
19
- range: range,
20
- stats: stats,
21
- activity_trends: stats[:activity_trends],
22
- stability: stats[:stability],
23
- storage_components: cache_storage_components,
24
- recent_events: recent_events(window)
25
- }
26
- end
9
+ def index
10
+ @component = "cache"
11
+ assign_cache_dashboard
12
+ end
27
13
 
28
- private
14
+ private
29
15
 
30
- def unavailable_assignments
31
- {
32
- cache_dashboard_available: false,
33
- storage_components: [],
34
- recent_events: [],
35
- activity_trends: SolidObserver::Services::CacheStats::ACTIVITY_TREND_EMPTY,
36
- stability: SolidObserver::Services::CacheStats::STABILITY_EMPTY
37
- }
16
+ def assign_cache_dashboard
17
+ unless SolidObserver.config.solid_cache_enabled?
18
+ @cache_dashboard_available = false
19
+ @storage_components = []
20
+ @recent_events = []
21
+ @activity_trends = SolidObserver::Services::CacheStats::ACTIVITY_TREND_EMPTY
22
+ @stability = SolidObserver::Services::CacheStats::STABILITY_EMPTY
23
+ return
38
24
  end
39
25
 
40
- def cache_storage_components
41
- SolidObserver::Services::StorageInfoSnapshot.call.select do |snapshot|
42
- CACHE_STORAGE_COMPONENTS.include?(snapshot[:component])
43
- end
44
- end
26
+ range = SolidObserver::Services::CacheStats.parse_range(request_range_param)
27
+ window = SolidObserver::Services::CacheStats.range_duration(range)
28
+ stats = SolidObserver::Services::CacheStats.call(window: window)
29
+
30
+ @cache_dashboard_available = true
31
+ @range = range
32
+ @stats = stats
33
+ @activity_trends = stats[:activity_trends]
34
+ @stability = stats[:stability]
35
+ @storage_components = cache_storage_components
36
+ @recent_events = recent_events(window)
37
+ end
45
38
 
46
- def recent_events(window)
47
- current_time = Time.current
48
- SolidObserver::CacheEvent.where(recorded_at: (current_time - window)..current_time).recent(10)
49
- rescue ActiveRecord::StatementInvalid
50
- []
39
+ def cache_storage_components
40
+ SolidObserver::Services::StorageInfoSnapshot.call.select do |snapshot|
41
+ CACHE_STORAGE_COMPONENTS.include?(snapshot[:component])
51
42
  end
52
43
  end
53
44
 
54
- def index
55
- @component = "cache"
56
- assign_cache_dashboard
45
+ def recent_events(window)
46
+ current_time = Time.current
47
+ SolidObserver::CacheEvent.where(recorded_at: (current_time - window)..current_time).recent(10)
48
+ rescue ActiveRecord::StatementInvalid
49
+ []
57
50
  end
58
51
  end
59
52
  end
@@ -11,7 +11,6 @@ module SolidObserver
11
11
 
12
12
  def index
13
13
  @component = selected_component
14
- return assign_cache_dashboard if @component == "cache"
15
14
 
16
15
  return unless @component == "queue" && SolidObserver.config.solid_queue_enabled?
17
16
 
@@ -20,6 +19,7 @@ module SolidObserver
20
19
  end
21
20
 
22
21
  def live_poll
22
+ expires_in 1.day, public: true
23
23
  send_file(
24
24
  SolidObserver::Engine.root.join("app/assets/javascripts/solid_observer/live_poll.js"),
25
25
  type: "application/javascript; charset=utf-8",
@@ -54,12 +54,6 @@ module SolidObserver
54
54
  @recent_events = QueueEvent.recent(10)
55
55
  end
56
56
 
57
- def assign_cache_dashboard
58
- SolidObserver::CacheDashboardController.cache_dashboard_assignments(range_param: request_range_param).each do |name, value|
59
- instance_variable_set("@#{name}", value)
60
- end
61
- end
62
-
63
57
  def request_range_param
64
58
  request&.query_parameters&.[]("range") || request&.query_parameters&.[](:range)
65
59
  end
@@ -156,6 +156,63 @@ module SolidObserver
156
156
  CACHE_RANGE_LABELS.fetch(range_key.to_s, "in selected range")
157
157
  end
158
158
 
159
+ def cable_range_label(range_key)
160
+ CACHE_RANGE_LABELS.fetch(range_key.to_s, "in selected range")
161
+ end
162
+
163
+ def cable_ratio_percent(value)
164
+ number_to_percentage(value.to_f * 100, precision: 1, strip_insignificant_zeros: true)
165
+ end
166
+
167
+ def cable_stability_badge(state)
168
+ stability_badge_for(state.to_sym)
169
+ end
170
+
171
+ def cable_stability_detail(stability)
172
+ state = (stability || {})[:state]&.to_sym
173
+ state = :stable unless STABILITY_STATES.key?(state)
174
+
175
+ case state
176
+ when :critical
177
+ critical_cable_stability_detail(stability)
178
+ when :degraded
179
+ degraded_cable_stability_detail(stability)
180
+ else
181
+ "No cable errors or subscription rejections in the selected range and backlog current snapshot is healthy"
182
+ end
183
+ end
184
+
185
+ def cable_event_digest(digest, visible_chars: 10)
186
+ digest = digest.to_s
187
+ return "—" if digest.empty?
188
+ return digest if digest.length <= visible_chars
189
+
190
+ "#{digest.first(visible_chars)}…"
191
+ end
192
+
193
+ # :reek:FeatureEnvy
194
+ def cable_backlog_summary(stats)
195
+ if stats[:backlog_available]
196
+ {
197
+ value: number_with_delimiter(stats[:backlog_count].to_i),
198
+ subtitle: "current Solid Cable snapshot"
199
+ }
200
+ else
201
+ {value: "—", subtitle: "current Solid Cable snapshot unavailable"}
202
+ end
203
+ end
204
+
205
+ def cable_storage_summary(storage_components)
206
+ snapshots = Array(storage_components)
207
+ reason = cable_storage_unavailable_reason(snapshots)
208
+ return {value: "—", subtitle: "— #{reason}"} if reason
209
+
210
+ {
211
+ value: number_to_human_size(cable_storage_total_bytes(snapshots), precision: 1, significant: false, strip_insignificant_zeros: false),
212
+ subtitle: "Cable telemetry + Solid Cable messages"
213
+ }
214
+ end
215
+
159
216
  def cache_stability_detail(stability)
160
217
  state = (stability || {})[:state]&.to_sym
161
218
  state = :stable unless STABILITY_STATES.key?(state)
@@ -189,6 +246,10 @@ module SolidObserver
189
246
  SolidObserver.config.solid_cache_enabled?
190
247
  end
191
248
 
249
+ def cable_component_enabled?
250
+ SolidObserver.config.solid_cable_enabled?
251
+ end
252
+
192
253
  def dashboard_section_active?(component)
193
254
  current_component = @component.presence || "queue"
194
255
  controller_name == "dashboard" && current_component == component.to_s
@@ -231,6 +292,59 @@ module SolidObserver
231
292
  snapshots.find { |snapshot| !snapshot[:available] }&.[](:unavailable_reason)
232
293
  end
233
294
 
295
+ # :reek:FeatureEnvy
296
+ # :reek:TooManyStatements
297
+ def critical_cable_stability_detail(stability)
298
+ parts = []
299
+ error_count = stability[:error_count].to_i
300
+ parts << pluralize(error_count, "cable error") if error_count.positive?
301
+
302
+ rejection_count = stability[:rejection_count].to_i
303
+ rejection_rate = stability[:rejection_rate].to_f
304
+ if rejection_rate > 0.0
305
+ parts << "#{cable_ratio_percent(rejection_rate)} rejection rate"
306
+ elsif rejection_count.positive?
307
+ parts << pluralize(rejection_count, "subscription rejection")
308
+ end
309
+
310
+ backlog_ratio = stability[:backlog_ratio].to_f
311
+ if backlog_ratio >= 0.5
312
+ parts << "backlog at #{number_to_percentage(backlog_ratio * 100, precision: 0)} in current snapshot"
313
+ end
314
+
315
+ return "Cable stability critical" if parts.empty?
316
+
317
+ "#{parts.join("; ")} in the selected range"
318
+ end
319
+
320
+ # :reek:FeatureEnvy
321
+ # :reek:TooManyStatements
322
+ def degraded_cable_stability_detail(stability)
323
+ return "Backlog current snapshot unavailable" unless stability[:backlog_available]
324
+
325
+ backlog_ratio = stability[:backlog_ratio].to_f
326
+ if backlog_ratio >= SolidObserver.config.cable_backlog_threshold.to_f
327
+ return "Backlog at #{number_to_percentage(backlog_ratio * 100, precision: 0)} in current snapshot"
328
+ end
329
+
330
+ rejection_count = stability[:rejection_count].to_i
331
+ if rejection_count.positive?
332
+ return "#{pluralize(rejection_count, "subscription rejection")} in the selected range"
333
+ end
334
+
335
+ "Cable stability degraded"
336
+ end
337
+
338
+ def cable_storage_total_bytes(snapshots)
339
+ snapshots.sum { |snapshot| snapshot[:db_size_bytes].to_i }
340
+ end
341
+
342
+ def cable_storage_unavailable_reason(snapshots)
343
+ return "Storage snapshot unavailable" unless snapshots.size == 2
344
+
345
+ snapshots.find { |snapshot| !snapshot[:available] }&.[](:unavailable_reason)
346
+ end
347
+
234
348
  def cache_event_outcome_meta(event)
235
349
  hit = event.hit
236
350
 
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidObserver
4
+ class CableEvent < BaseEvent
5
+ self.table_name = "solid_observer_cable_events"
6
+
7
+ validates :event_type, presence: true
8
+ validates :recorded_at, presence: true
9
+
10
+ scope :errored, -> { where.not(error_class: nil) }
11
+ scope :recent, ->(limit = 10) { order(recorded_at: :desc).limit(limit) }
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidObserver
4
+ class CableMetric < BaseRecord
5
+ self.table_name = "solid_observer_cable_metrics"
6
+
7
+ validates :period_start, presence: true
8
+ validates :broadcasts_count, :transmissions_count, :confirmations_count,
9
+ :rejections_count, :perform_actions_count, :errors_count,
10
+ numericality: {only_integer: true, greater_than_or_equal_to: 0}
11
+ end
12
+ end
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidObserver
4
- class CacheMetric < BaseMetric
4
+ class CacheMetric < BaseRecord
5
5
  self.table_name = "solid_observer_cache_metrics"
6
- clear_validators!
7
6
 
8
7
  validates :event_type, presence: true, length: {maximum: 64}
9
8
  validates :period_start, presence: true
@@ -6,7 +6,7 @@ module SolidObserver
6
6
 
7
7
  MB_TO_BYTES = 1_048_576
8
8
  GB_TO_BYTES = 1_073_741_824
9
- COMPONENTS = %w[queue_observer cache_observer solid_cache].freeze
9
+ COMPONENTS = %w[queue_observer cache_observer solid_cache cable_observer solid_cable].freeze
10
10
 
11
11
  validates :db_size_bytes, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
12
12
  validates :event_count, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}