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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +80 -20
- 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 +33 -40
- data/app/controllers/solid_observer/dashboard_controller.rb +1 -7
- data/app/helpers/solid_observer/application_helper.rb +114 -0
- 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_metric.rb +1 -2
- data/app/models/solid_observer/storage_info.rb +1 -1
- data/app/views/layouts/solid_observer/application.html.erb +19 -8
- 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/dashboard/_queue_table.html.erb +1 -0
- data/app/views/solid_observer/dashboard/index.html.erb +2 -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/storages/show.html.erb +29 -3
- data/config/routes.rb +2 -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 +18 -3
- 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 +11 -36
- data/lib/solid_observer/cache_metric_buffer.rb +229 -0
- data/lib/solid_observer/chart_buffer.rb +84 -27
- data/lib/solid_observer/configuration.rb +47 -4
- data/lib/solid_observer/engine.rb +46 -28
- 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_stats.rb +35 -18
- data/lib/solid_observer/services/cleanup_storage.rb +82 -47
- 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_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 +23 -0
- data/lib/solid_observer/services/record_cache_metric.rb +13 -21
- data/lib/solid_observer/services/storage_info_snapshot.rb +103 -15
- data/lib/solid_observer/version.rb +1 -1
- data/lib/solid_observer.rb +36 -11
- data/lib/tasks/solid_observer.rake +84 -23
- metadata +26 -6
- data/app/assets/stylesheets/solid_observer/application.css +0 -18
- data/bin/console +0 -11
- data/bin/quality_gate +0 -95
- data/bin/setup +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 563c4874ef7e6a8ad8a485d0b3f86c9d1d8f924d5308d44c5137f76bda7e03de
|
|
4
|
+
data.tar.gz: 88ba3331b45977349604047db1a4e3e98efb17ebc1eca8d4c52a96825d320a83
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
>
|
|
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,
|
|
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
|
|
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 | ✅
|
|
477
|
-
| v0.5.0 | Solid Cable monitoring |
|
|
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
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
+
private
|
|
29
15
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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 <
|
|
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}
|