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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/README.md +134 -81
  4. data/app/assets/stylesheets/solid_observer/application.css +18 -0
  5. data/app/controllers/solid_observer/cache_dashboard_controller.rb +59 -0
  6. data/app/controllers/solid_observer/cache_operations_controller.rb +24 -0
  7. data/app/controllers/solid_observer/dashboard_controller.rb +44 -1
  8. data/app/controllers/solid_observer/storages_controller.rb +1 -1
  9. data/app/helpers/solid_observer/application_helper.rb +154 -5
  10. data/app/helpers/solid_observer/dashboard_helper.rb +30 -11
  11. data/app/models/solid_observer/cache_event.rb +15 -0
  12. data/app/models/solid_observer/cache_metric.rb +14 -0
  13. data/app/models/solid_observer/storage_info.rb +4 -1
  14. data/app/views/layouts/solid_observer/application.html.erb +144 -17
  15. data/app/views/solid_observer/cache_dashboard/_charts.html.erb +40 -0
  16. data/app/views/solid_observer/cache_dashboard/_recent_events.html.erb +34 -0
  17. data/app/views/solid_observer/cache_dashboard/_summary.html.erb +39 -0
  18. data/app/views/solid_observer/cache_dashboard/index.html.erb +62 -0
  19. data/app/views/solid_observer/cache_operations/_confirm_clear.html.erb +6 -0
  20. data/app/views/solid_observer/cache_operations/index.html.erb +60 -0
  21. data/app/views/solid_observer/dashboard/index.html.erb +34 -4
  22. data/app/views/solid_observer/jobs/show.html.erb +3 -3
  23. data/app/views/solid_observer/storages/show.html.erb +64 -32
  24. data/bin/quality_gate +1 -1
  25. data/config/routes.rb +5 -0
  26. data/db/migrate/20260601000001_create_solid_observer_cache_events.rb +22 -0
  27. data/db/migrate/20260601000002_create_solid_observer_cache_metrics.rb +18 -0
  28. data/db/migrate/20260602000001_add_component_to_solid_observer_storage_infos.rb +8 -0
  29. data/lib/generators/solid_observer/templates/initializer.rb.tt +2 -1
  30. data/lib/solid_observer/cache_event_buffer.rb +53 -0
  31. data/lib/solid_observer/cache_subscriber.rb +47 -0
  32. data/lib/solid_observer/cli/storage.rb +16 -13
  33. data/lib/solid_observer/configuration.rb +22 -3
  34. data/lib/solid_observer/engine.rb +44 -7
  35. data/lib/solid_observer/services/cache_operations.rb +115 -0
  36. data/lib/solid_observer/services/cache_stats.rb +329 -0
  37. data/lib/solid_observer/services/cleanup_storage.rb +18 -2
  38. data/lib/solid_observer/services/database_size.rb +13 -8
  39. data/lib/solid_observer/services/flush_cache_event_buffer.rb +54 -0
  40. data/lib/solid_observer/services/record_cache_event.rb +142 -0
  41. data/lib/solid_observer/services/record_cache_metric.rb +74 -0
  42. data/lib/solid_observer/services/storage_info_snapshot.rb +128 -0
  43. data/lib/solid_observer/version.rb +1 -1
  44. data/lib/tasks/solid_observer.rake +29 -0
  45. metadata +23 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a745bb3571f70b207e14f2e88f6162db6b2322a889d42f5ebd2a50e80c5b8014
4
- data.tar.gz: d05f835df0503f04f88223c0879145a2dab13279cd5bfc59346545c0e8263f1c
3
+ metadata.gz: a9c3e774c9276371b59f696226979b922772191e0531a4d9ba2dbb09b8e3e452
4
+ data.tar.gz: 570ae5aacef981358f68998d3df14bfc532e2af48cbdd21c6cd18ecbfa7132e7
5
5
  SHA512:
6
- metadata.gz: 8417c8013a7c9ff03a9d52481af63889adaba093abf0c9b041842664af7fb15edde929f7c7b94c6df12cbac00ca41d2aeee0f488fd7ffe831abf9d5f873bff09
7
- data.tar.gz: 92147e32675291ec743e055c55382bf719a9ab6dd10e2e507789d0163b703e06490ad97dbb785e0a9c0b51b8eb72eb7a849d95504ac52c8680e53a137b50e4e4
6
+ metadata.gz: 50684bd0b286fc9b93f77a57f5d0b60c3e0334b6d7804bf63307b8cdc05a78cab5bb586305b96617f5fe0e76f76cd89112c6cb6e059a45d190ca6dc7c49fa885
7
+ data.tar.gz: 9e456400209630d4e3b777355bf1d1a651e0bac1d90d6d40de4272c9cce8afb322bf5ff9afed10fe1a9e0911c7e84c15d02e6b2de3360e04f5d4d349bbf02f34
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## [0.4.0] - 2026-06-03
2
+
3
+ Headline: **Solid Cache observability** — multi-component foundation, cache dashboard, operational controls, and shared storage health.
4
+
5
+ ### Added
6
+ - **Multi-component SolidObserver foundation** (SO-070) — refactored engine, configuration, and storage to support multiple Solid Stack components alongside Solid Queue. Introduced `observe_cache` config flag (default `false`).
7
+ - **Cache telemetry foundation** (SO-071) — `CacheSubscriber` hooks into `ActiveSupport::Notifications` cache events; collects hit/miss/write/delete/prune operations with duration and error tracking. Keys and values are never recorded.
8
+ - **Cache metrics aggregation** (SO-072) — `CacheMetricsAggregator` computes hit rate, operations/sec, error rate, and average duration from raw cache events; scoped to configurable time windows.
9
+ - **Multi-component storage health** (SO-073) — Storage page aggregates Queue Observer DB, Cache Observer, and SolidCache table sizes into unified per-component rows with status indicators.
10
+ - **Cache operational controls** (SO-074) — Cache controls dashboard page; `solid_observer:cache:clear` and `solid_observer:cache:prune` rake tasks; CSRF-protected prune/clear actions in the Web UI.
11
+ - **Cache dashboard** (SO-075) — `/solid_observer/cache_dashboard` — hit rate, ops/sec, error rate, avg duration, storage footprint, activity trend sparklines, and stability pill.
12
+ - **Cache overview activity trends and health parity** (SO-077) — activity trend sparklines on the cache dashboard; stability indicator parity with the Queue dashboard.
13
+
14
+ ### Fixed
15
+ - **SolidCache storage snapshot TypeError** (SO-076) — `StorageInfoSnapshot` raised `TypeError: nil can't be coerced into Integer` on PostgreSQL hosts when SolidCache table names resolved to `nil`. Fixed with a nil-table-name guard on the PG size query path.
16
+ - **Failed-job error payload read as Hash** (SO-079) — `Jobs::Show` raised `NoMethodError` when the serialised `SolidQueue::FailedExecution#error` payload was a plain `Hash` instead of an `ActiveSupport::HashWithIndifferentAccess`. Now reads both representations safely.
17
+
18
+ ### Changed
19
+ - **Cross-tab styling consistency** (SO-078) — unified badge colours, font sizes, and border radii across Queue and Cache dashboard tabs so both surfaces share the same visual language.
20
+ - **Dashboard headers, select heights, and Storage nav** (SO-080) — normalised `<h1>` sizing, select control heights, and Storage navigation link to be consistent across all component tabs; Storage promoted to top-level nav item.
21
+
1
22
  ## [0.3.0] - 2026-05-25
2
23
 
3
24
  > Note: there is no separate `0.2.0` gem release — work originally scoped for v0.2.0 (stability + refactoring, SO-040 through SO-049) was folded into this release.
data/README.md CHANGED
@@ -7,35 +7,39 @@
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.3.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.4.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
- <a href="https://github.com/bart-oz/solid_observer/actions"><img src="https://img.shields.io/badge/coverage-96.98%25-brightgreen.svg" alt="Coverage"></a>
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>
14
14
  </p>
15
15
 
16
16
  ---
17
17
  <p align="center">
18
- <img src=".github/assets/dash_1.png" alt="Dashboard overview" width="700">
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. Starting with **Solid Queue** monitoring in v0.3.0, it provides unified visibility into your background job processing with a 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.4.0 covers both **Solid Queue** and **Solid Cache** with a unified Web UI dashboard, CLI tools, metrics collection, and distributed tracing support.
22
22
 
23
- ## Features (v0.3.0)
23
+ ## Features (v0.4.0)
24
24
 
25
- - 🖥️ **Web UI Dashboard** Live queue stats, job browser, event log, and storage info
26
- - 📊 **Real-time Queue Status** — Monitor jobs across all states (ready, scheduled, claimed, failed)
27
- - 🔍 **Job Management CLI** List, inspect, retry, and discard failed jobs
28
- - 💾 **Storage Monitoring** Track database size and event counts
29
- - 🔗 **Distributed Tracing** Correlate jobs with APM tools (Datadog, Sentry, OpenTelemetry)
30
- - **High Performance** Buffered writes, configurable sampling, minimal overhead
31
- - 🛡️ **Production Ready** Docker/CI/K8s safe boot, automatic cleanup, size limits, retention policies
32
- - 🚀 **Two Operating Modes** Real-time (no migrations) or persistence (full event history)
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 |
35
+
36
+ Additional: 🔗 APM distributed tracing · ⚡ buffered writes · 🛡️ Docker/CI/K8s safe boot
33
37
 
34
38
  ## Requirements
35
39
 
36
40
  - Ruby 3.2+
37
41
  - Rails 8.0+
38
- - Solid Queue (properly configured for all environments)
42
+ - Solid Queue (configured with `connects_to` in all environments)
39
43
 
40
44
  > **Note:** Ensure Solid Queue is configured with `connects_to` in all environments, not just production. See [Troubleshooting](#troubleshooting) if you encounter database connection issues.
41
45
 
@@ -169,14 +173,6 @@ Configuration:
169
173
 
170
174
  SolidObserver ships with a zero-dependency Web UI (no asset pipeline, no JS framework) at `/solid_observer`.
171
175
 
172
- <table>
173
- <tr>
174
- <td align="center"><a href=".github/assets/dash_1.png"><img src=".github/assets/dash_1.png" alt="Dashboard overview" width="250"></a></td>
175
- <td align="center"><a href=".github/assets/dash_2.png"><img src=".github/assets/dash_2.png" alt="Dashboard jobs and events" width="250"></a></td>
176
- <td align="center"><a href=".github/assets/dash_3.png"><img src=".github/assets/dash_3.png" alt="Dashboard storage and details" width="250"></a></td>
177
- </tr>
178
- </table>
179
-
180
176
  ### Mount
181
177
 
182
178
  The install generator mounts it for you. To mount manually:
@@ -186,7 +182,7 @@ The install generator mounts it for you. To mount manually:
186
182
  mount SolidObserver::Engine, at: "/solid_observer"
187
183
  ```
188
184
 
189
- ### Configuration
185
+ ### Auth Configuration
190
186
 
191
187
  ```ruby
192
188
  SolidObserver.configure do |config|
@@ -200,43 +196,95 @@ end
200
196
  | Option | Default | Purpose |
201
197
  |---|---|---|
202
198
  | `ui_enabled` | `!Rails.env.production?` | Master switch for the Web UI |
203
- | `ui_username` / `ui_password` | `nil` | HTTP Basic Auth credentials. Auth is enabled only when **both** are set; if either is missing or `nil`, the UI is unauthenticated |
204
- | `ui_base_controller` | `"ApplicationController"` | Name of your host app's base controller. SolidObserver does **not** inherit from it; the value is used to detect API-only apps so the engine can include the rendering modules its dashboard needs |
199
+ | `ui_username` / `ui_password` | `nil` | HTTP Basic Auth. Auth is enabled only when **both** are set |
200
+ | `ui_base_controller` | `"ApplicationController"` | Detect API-only apps to include rendering modules |
205
201
 
206
- Live polling cadence is hardcoded at 5s.
202
+ For production hardening (fail-loud on missing env var vs. fail-open), see [Configuration](#configuration) below.
207
203
 
208
- ### Production hardening (recommended)
204
+ ### Caveats
205
+
206
+ - **Realtime mode** — Events and Storage navigation links are hidden. Direct visits redirect with a flash alert.
207
+ - **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
+ - **Host-app callbacks are not inherited.** Use `ui_username` / `ui_password` for built-in HTTP Basic Auth.
209
+ - **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`.
210
+
211
+ See the full component breakdown in [Components](#components) below.
212
+
213
+ ## Components
214
+
215
+ ### Solid Queue Observability
216
+
217
+ Live queue stats, job browser (all states: ready/scheduled/claimed/failed), events log, time-range selector (15m → 14d), sparklines for Performed/Ready/Failed, stability indicator, retry/discard with confirmation dialogs. Realtime and persistence modes supported.
218
+
219
+ <table>
220
+ <tr>
221
+ <td align="center"><a href=".github/assets/dash_2.png"><img src=".github/assets/dash_2.png" alt="Queue overview — 1d range" width="340"></a></td>
222
+ <td align="center"><a href=".github/assets/dash_3.png"><img src=".github/assets/dash_3.png" alt="Jobs list" width="340"></a></td>
223
+ </tr>
224
+ <tr>
225
+ <td align="center"><a href=".github/assets/dash_4.png"><img src=".github/assets/dash_4.png" alt="Failed job detail" width="340"></a></td>
226
+ <td align="center"><a href=".github/assets/dash_5.png"><img src=".github/assets/dash_5.png" alt="Events stream" width="340"></a></td>
227
+ </tr>
228
+ </table>
209
229
 
210
- The snippet above silently disables auth if `ENV["SOLID_OBSERVER_PASSWORD"]` is unset (fail-open with a boot warning — see Caveats). For production, prefer one of these patterns so a missing env var fails loudly at boot rather than shipping an unauthenticated UI:
230
+ **CLI commands (Solid Queue):**
231
+
232
+ ```bash
233
+ bin/rails solid_observer:status # Queue overview
234
+ bin/rails solid_observer:jobs:list # All active jobs
235
+ bin/rails "solid_observer:jobs:list[failed]" # Failed jobs
236
+ bin/rails "solid_observer:jobs:show[JOB_ID]" # Job details
237
+ bin/rails "solid_observer:jobs:retry[JOB_ID]" # Retry failed job
238
+ bin/rails "solid_observer:jobs:discard[JOB_ID]" # Discard failed job
239
+ bin/rails solid_observer:buffer:flush # Flush event buffer
240
+ bin/rails solid_observer:buffer:clear # Clear buffer without saving
241
+ ```
242
+
243
+ ### Solid Cache Observability
244
+
245
+ Cache dashboard shows hit rate, operations/sec, error rate, average duration, storage footprint, activity trend sparklines, and a stability pill. Cache controls page provides prune and clear operations. **Keys and values are never shown** — only aggregate metrics are collected.
246
+
247
+ <table>
248
+ <tr>
249
+ <td align="center"><a href=".github/assets/dash_6.png"><img src=".github/assets/dash_6.png" alt="Cache overview dashboard" width="340"></a></td>
250
+ <td align="center"><a href=".github/assets/dash_7.png"><img src=".github/assets/dash_7.png" alt="Cache operational controls" width="340"></a></td>
251
+ </tr>
252
+ </table>
253
+
254
+ **Enabling Cache observability:**
255
+
256
+ SolidCache must be configured in your host app (Rails 8 default). Then enable in the SolidObserver initializer:
211
257
 
212
258
  ```ruby
213
- # Option A: fail at boot if the password env var is missing
214
259
  SolidObserver.configure do |config|
215
- config.ui_username = "admin"
216
- config.ui_password = ENV.fetch("SOLID_OBSERVER_PASSWORD") # raises KeyError if unset
260
+ config.observe_cache = true # default: false
217
261
  end
262
+ ```
218
263
 
219
- # Option B: only enable auth when the env var is present (no auth otherwise)
220
- SolidObserver.configure do |config|
221
- if (password = ENV["SOLID_OBSERVER_PASSWORD"]).present?
222
- config.ui_username = "admin"
223
- config.ui_password = password
224
- end
225
- end
264
+ `solid_cache_enabled?` is `true` when both `observe_cache = true` and SolidCache is available in the host app.
265
+
266
+ **CLI commands (Solid Cache):**
267
+
268
+ ```bash
269
+ bin/rails solid_observer:cache:clear # Clear all SolidCache entries (with confirmation)
270
+ bin/rails solid_observer:cache:prune # Prune expired SolidCache entries
226
271
  ```
227
272
 
228
- Option A is best when the UI must always be authenticated in production (a missing env var crashes boot — you find out immediately). Option B is best when the UI is optional in some environments.
273
+ ### Storage
229
274
 
230
- ### Caveats
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.
231
276
 
232
- - **Realtime mode** (`storage_mode: :realtime`) — Events and Storage navigation links are hidden. Direct visits to `/solid_observer/events` or `/solid_observer/storage` redirect to the dashboard with a flash alert (`This page is not available in real-time mode`).
233
- - **API-only apps** — the Web UI works in API-only Rails apps without manual configuration. SolidObserver's engine ships its own Cookies/Session/Flash middleware stack scoped to `/solid_observer/*` requests, so the dashboard renders, flash messages display, and retry/discard CSRF forms work even when the host app has `config.api_only = true`. The host app's middleware stack is not modified.
234
- - **Custom API base controller** — if your host app's API base controller is **not** named `ApplicationController`, set `config.ui_base_controller` to its name (e.g. `"Api::BaseController"`). The engine uses this only to detect `ActionController::API` ancestry; if detected, it includes `ActionView::Layouts`, `ActionView::Rendering`, and `ActionController::RequestForgeryProtection` so layouts and CSRF forms render.
235
- - **Host-app callbacks are not inherited.** SolidObserver does not run your host app's `before_action`s or authentication. Use `ui_username` / `ui_password` for the engine's built-in HTTP Basic Auth.
236
- - **Auth misconfiguration is fail-open, but loud.** If you set `ui_username` but `ui_password` resolves to `nil` (e.g. an unset ENV var), the UI ships unauthenticated rather than locking everyone out — and the engine logs a `WARNING` at boot naming the missing credential. Verify both are set in production.
277
+ <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>
279
+ </p>
280
+
281
+ For adapter notes and multi-adapter setup, see [Database Setup](#database-setup-persistence-mode) below.
237
282
 
238
283
  ## Configuration
239
284
 
285
+ <details>
286
+ <summary>Show full configuration reference</summary>
287
+
240
288
  After installation, configure SolidObserver in `config/initializers/solid_observer.rb`:
241
289
 
242
290
  ```ruby
@@ -249,6 +297,9 @@ SolidObserver.configure do |config|
249
297
  # Enable queue monitoring (default: true)
250
298
  config.observe_queue = true
251
299
 
300
+ # Enable cache monitoring (default: false; requires SolidCache in host app)
301
+ config.observe_cache = true
302
+
252
303
  # Data Retention (persistence mode only)
253
304
  config.event_retention = 30.days # Keep events for 30 days
254
305
  config.metrics_retention = 90.days # Keep metrics for 90 days
@@ -266,8 +317,6 @@ end
266
317
 
267
318
  ### APM Integration
268
319
 
269
- Connect SolidObserver with your Application Performance Monitoring tool for distributed tracing:
270
-
271
320
  ```ruby
272
321
  SolidObserver.configure do |config|
273
322
  # Datadog APM
@@ -294,6 +343,26 @@ end
294
343
 
295
344
  When configured, all job events will include your correlation ID, allowing you to trace jobs back to the originating request.
296
345
 
346
+ ### Production hardening (recommended)
347
+
348
+ ```ruby
349
+ # Option A: fail at boot if the password env var is missing
350
+ SolidObserver.configure do |config|
351
+ config.ui_username = "admin"
352
+ config.ui_password = ENV.fetch("SOLID_OBSERVER_PASSWORD") # raises KeyError if unset
353
+ end
354
+
355
+ # Option B: only enable auth when the env var is present (no auth otherwise)
356
+ SolidObserver.configure do |config|
357
+ if (password = ENV["SOLID_OBSERVER_PASSWORD"]).present?
358
+ config.ui_username = "admin"
359
+ config.ui_password = password
360
+ end
361
+ end
362
+ ```
363
+
364
+ </details>
365
+
297
366
  ## CLI Reference
298
367
 
299
368
  **Available in both modes (real-time and persistence):**
@@ -315,8 +384,10 @@ When configured, all job events will include your correlation ID, allowing you t
315
384
  | `solid_observer:buffer:clear` | Clear buffer without saving |
316
385
  | `solid_observer:storage:cleanup` | Run retention-based cleanup |
317
386
  | `solid_observer:storage:purge` | Delete ALL SolidObserver data |
387
+ | `solid_observer:cache:clear` | Clear all SolidCache entries (with confirmation) |
388
+ | `solid_observer:cache:prune` | Prune expired SolidCache entries |
318
389
 
319
- > **Note:** Storage commands manage **SolidObserver's storage** (event logs, metrics, snapshots) — not Solid Queue's jobs. To manage jobs, use `jobs:discard` or `jobs:retry`.
390
+ > **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.
320
391
 
321
392
  ### Jobs List Arguments
322
393
 
@@ -330,38 +401,24 @@ Arguments are positional: `[status, queue, job_class, limit]`
330
401
  | 4th | Max results (default: 20) | `50` |
331
402
 
332
403
  ```bash
333
- # Examples
334
404
  bin/rails solid_observer:jobs:list # All ready jobs
335
405
  bin/rails "solid_observer:jobs:list[failed]" # Failed jobs
336
406
  bin/rails "solid_observer:jobs:list[ready,mailers]" # Ready jobs in mailers queue
337
407
  bin/rails "solid_observer:jobs:list[failed,,,50]" # 50 failed jobs (skip queue/class)
338
408
  ```
339
409
 
340
- ### Buffer & Storage Management (Persistence Mode)
341
-
342
- ```bash
343
- # Flush in-memory buffer to database
344
- bin/rails solid_observer:buffer:flush
345
-
346
- # Clear buffer without saving (loses pending events!)
347
- bin/rails solid_observer:buffer:clear
348
-
349
- # Run cleanup based on retention policy (default: 30 days)
350
- bin/rails solid_observer:storage:cleanup
351
-
352
- # Delete ALL SolidObserver data (events + snapshots, interactive confirmation)
353
- bin/rails solid_observer:storage:purge
354
- ```
355
-
356
- > **Important:** `storage:purge` deletes SolidObserver's monitoring data, NOT your Solid Queue jobs. Your queued jobs remain safe.
357
-
358
410
  ## Database Setup (Persistence Mode)
359
411
 
360
412
  > **Tip:** If you're using real-time mode (`storage_mode: :realtime`), you can skip this section entirely — no database setup is needed.
361
413
 
362
414
  **SolidObserver works with any main application database** — PostgreSQL, MySQL, or SQLite.
363
415
 
364
- For its own monitoring data, the install generator defaults to a **separate SQLite database** — file-based, no extra infrastructure needed. The example below is what `rails generate solid_observer:install` produces:
416
+ For its own monitoring data, the install generator defaults to a **separate SQLite database** — file-based, no extra infrastructure needed.
417
+
418
+ <details>
419
+ <summary>Show multi-adapter setup and configuration details</summary>
420
+
421
+ The example below is what `rails generate solid_observer:install` produces:
365
422
 
366
423
  ```yaml
367
424
  # config/database.yml (generator default)
@@ -371,16 +428,16 @@ solid_observer_queue:
371
428
  database: storage/<%= Rails.env %>_solid_observer_queue.sqlite3
372
429
  ```
373
430
 
374
- > **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, but the size column on the dashboard will be empty until adapter support is added.
431
+ > **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.
375
432
 
376
433
  ### Multi-adapter setup
377
434
 
378
- If your host application uses one database adapter (e.g. PostgreSQL) and you want SolidObserver to use a different adapter (e.g. SQLite — for isolation, simpler ops, or to avoid loading observability traffic onto your primary DB), keep the `solid_observer_queue` block **self-contained** — do not rely on `<<: *default`. The generator default (shown above) uses `<<: *default` with an explicit `adapter: sqlite3` override; that is safe on SQLite-primary hosts where the anchor is also SQLite. On a PostgreSQL host, merging `<<: *default` without an explicit adapter override pulls the PG adapter into the observer connection and fails at `db:create` with `PG::SyntaxError` (PG treating a SQLite file path as a database name). For multi-adapter connections, omit the merge entirely, as shown below.
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:
379
436
 
380
437
  ```yaml
381
438
  # config/database.yml
382
439
  default: &default
383
- adapter: postgresql # host's adapter
440
+ adapter: postgresql
384
441
  encoding: unicode
385
442
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
386
443
 
@@ -388,7 +445,6 @@ development:
388
445
  primary:
389
446
  <<: *default
390
447
  database: my_app_development
391
- # ... cache / queue / cable on PG ...
392
448
 
393
449
  solid_observer_queue:
394
450
  adapter: sqlite3 # explicit override — do NOT merge *default
@@ -398,7 +454,7 @@ development:
398
454
  migrations_paths: db/solid_observer_migrate
399
455
  ```
400
456
 
401
- Apply the same pattern to `test:` and `production:`. For production with SQLite, ensure the `storage/` path is on persistent disk (not ephemeral container storage) — otherwise prefer PostgreSQL for the observer DB too.
457
+ Apply the same pattern to `test:` and `production:`. For production with SQLite, ensure the `storage/` path is on persistent disk.
402
458
 
403
459
  **Bundler note (PG-only hosts):** if your `Gemfile` does not already include `sqlite3`, add it:
404
460
 
@@ -406,20 +462,18 @@ Apply the same pattern to `test:` and `production:`. For production with SQLite,
406
462
  gem "sqlite3", "~> 2.0"
407
463
  ```
408
464
 
409
- then `bundle install`. SolidObserver does not declare `sqlite3` as a runtime dependency adapter choice is yours.
465
+ **`migrations_paths` is recommended** to isolate SolidObserver's migrations from the host's primary `db/migrate/` folder. When `migrations_paths` is set on `solid_observer_queue`, `bin/rails solid_observer:install:migrations` copies migrations to that path automatically.
410
466
 
411
- **`migrations_paths` is recommended** to isolate SolidObserver's migrations from the host's primary `db/migrate/` folder. This prevents Rails from running SolidObserver's migrations against your primary database (which would create an unused `solid_observer_queue_events` table there). When `migrations_paths` is set on `solid_observer_queue`, `bin/rails solid_observer:install:migrations` copies migrations to that path automatically — no manual `mv` required.
467
+ </details>
412
468
 
413
469
  ## Roadmap
414
470
 
415
- SolidObserver is actively developed. Here's what's coming:
416
-
417
471
  | Version | Focus | Status |
418
472
  |---------|-------|--------|
419
473
  | v0.1.0 | Solid Queue monitoring, CLI tools | ✅ Released |
420
474
  | v0.1.1 | Real-time mode (no migrations needed) | ✅ Released |
421
- | v0.3.0 | Web UI dashboard + stability hardening | ✅ Current |
422
- | v0.4.0 | Solid Cache monitoring | 🔜 Planned |
475
+ | v0.3.0 | Web UI dashboard + stability hardening | ✅ Released |
476
+ | v0.4.0 | Solid Cache monitoring | Current |
423
477
  | v0.5.0 | Solid Cable monitoring | 🔜 Planned |
424
478
  | v0.6.0 | Cross-component correlation, health scores | 🔜 Planned |
425
479
  | v0.7.0 | Alerting & notifications | 🔜 Planned |
@@ -478,7 +532,7 @@ config.solid_queue.connects_to = { database: { writing: :queue } }
478
532
 
479
533
  ### Multi-database setup
480
534
 
481
- > **See also:** [Multi-adapter setup](#multi-adapter-setup) in the Database Setup section for the full pattern (explicit `adapter:` override, `gem "sqlite3"` Bundler note, `migrations_paths` rationale).
535
+ > **See also:** [Multi-adapter setup](#multi-adapter-setup) in the Database Setup section for the full pattern.
482
536
 
483
537
  SolidObserver works with Rails multi-database configurations. Quick-reference example with PostgreSQL as your primary database and SQLite for SolidObserver's storage:
484
538
 
@@ -487,7 +541,6 @@ development:
487
541
  primary:
488
542
  adapter: postgresql
489
543
  database: myapp_development
490
- # ... PostgreSQL settings
491
544
  solid_observer_queue:
492
545
  adapter: sqlite3
493
546
  database: storage/development_solid_observer_queue.sqlite3
@@ -0,0 +1,18 @@
1
+ .so-sidebar__nav .active {
2
+ font-weight: 600;
3
+ }
4
+
5
+ .so-sidebar__nav a:focus-visible {
6
+ outline: 2px solid var(--so-info);
7
+ outline-offset: 2px;
8
+ box-shadow: 0 0 0 3px var(--so-focus-ring);
9
+ }
10
+
11
+ .so-sidebar__section {
12
+ font-size: 0.7rem;
13
+ text-transform: uppercase;
14
+ letter-spacing: 0.08em;
15
+ color: var(--so-text-muted);
16
+ padding: 0.5rem 0.75rem 0.25rem;
17
+ margin: 0.5rem 0.75rem 0.25rem;
18
+ }
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dashboard_controller"
4
+
5
+ module SolidObserver
6
+ class CacheDashboardController < DashboardController
7
+ CACHE_STORAGE_COMPONENTS = %w[solid_cache cache_observer].freeze
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
27
+
28
+ private
29
+
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
+ }
38
+ end
39
+
40
+ def cache_storage_components
41
+ SolidObserver::Services::StorageInfoSnapshot.call.select do |snapshot|
42
+ CACHE_STORAGE_COMPONENTS.include?(snapshot[:component])
43
+ end
44
+ end
45
+
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
+ []
51
+ end
52
+ end
53
+
54
+ def index
55
+ @component = "cache"
56
+ assign_cache_dashboard
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidObserver
4
+ class CacheOperationsController < ApplicationController
5
+ def index
6
+ @cache_controls_available = SolidObserver::Services::CacheOperations.available?
7
+ end
8
+
9
+ def prune
10
+ redirect_with_result(SolidObserver::Services::CacheOperations.prune)
11
+ end
12
+
13
+ def clear
14
+ redirect_with_result(SolidObserver::Services::CacheOperations.clear)
15
+ end
16
+
17
+ private
18
+
19
+ def redirect_with_result(result)
20
+ flash_key = result[:ok] ? :notice : :alert
21
+ redirect_to cache_operations_path, flash_key => result[:message]
22
+ end
23
+ end
24
+ end
@@ -1,11 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../helpers/solid_observer/application_helper"
4
+
3
5
  module SolidObserver
4
6
  class DashboardController < ApplicationController
7
+ helper SolidObserver::ApplicationHelper
8
+
5
9
  skip_forgery_protection only: :live_poll
6
10
  skip_after_action :verify_same_origin_request, only: :live_poll
7
11
 
8
12
  def index
13
+ @component = selected_component
14
+ return assign_cache_dashboard if @component == "cache"
15
+
16
+ return unless @component == "queue" && SolidObserver.config.solid_queue_enabled?
17
+
9
18
  assign_range_and_stats
10
19
  load_persistence_data if persistence_mode?
11
20
  end
@@ -32,13 +41,25 @@ module SolidObserver
32
41
  @range = range
33
42
  @live = request_live_param == "on"
34
43
  @stats = QueueStats.snapshot(range: range)
35
- @chart = QueueStats.chart_data(window: QueueStats.range_duration(@range))
44
+ @chart = if @stats[:available]
45
+ QueueStats.chart_data(window: QueueStats.range_duration(@range))
46
+ else
47
+ {performed: [], failed: [], ready: []}
48
+ end
49
+ rescue
50
+ @chart = {performed: [], failed: [], ready: []}
36
51
  end
37
52
 
38
53
  def load_persistence_data
39
54
  @recent_events = QueueEvent.recent(10)
40
55
  end
41
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
+
42
63
  def request_range_param
43
64
  request&.query_parameters&.[]("range") || request&.query_parameters&.[](:range)
44
65
  end
@@ -75,5 +96,27 @@ module SolidObserver
75
96
  def append_chart_buffer
76
97
  ChartBuffer.append(SolidQueue::ReadyExecution.count) if QueueStats.solid_queue_available?
77
98
  end
99
+
100
+ def selected_component
101
+ requested = if request&.respond_to?(:path_parameters)
102
+ request.path_parameters&.[](:component).to_s
103
+ else
104
+ ""
105
+ end
106
+ requested = path_component if requested.empty?
107
+ return "cache" if requested == "cache" && SolidObserver.config.solid_cache_enabled?
108
+
109
+ "queue"
110
+ end
111
+
112
+ def path_component
113
+ return "" unless request&.respond_to?(:path)
114
+
115
+ path = request&.path.to_s
116
+ return "cache" if path.end_with?("/cache")
117
+ return "queue" if path.end_with?("/queue")
118
+
119
+ ""
120
+ end
78
121
  end
79
122
  end
@@ -5,7 +5,7 @@ module SolidObserver
5
5
  include RequirePersistenceMode
6
6
 
7
7
  def show
8
- @current_storage = SolidObserver::StorageInfo.order(recorded_at: :desc).first
8
+ @storage_components = SolidObserver::Services::StorageInfoSnapshot.call
9
9
  @storage_history = SolidObserver::StorageInfo.recent(20)
10
10
  end
11
11
  end