railwatch 0.5.1 → 0.6.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/AGENTS.md +6 -2
- data/CHANGELOG.md +119 -0
- data/README.md +21 -13
- data/app/controllers/railwatch/dashboard_controller.rb +1 -0
- data/app/models/railwatch/application_record.rb +2 -2
- data/app/models/railwatch/telemetry_record.rb +2 -2
- data/docs/configuration.md +29 -13
- data/docs/embedded.md +32 -12
- data/docs/faq.md +6 -5
- data/docs/getting-started.md +9 -5
- data/docs/troubleshooting.md +10 -6
- data/lib/generators/railwatch/install/install_generator.rb +31 -16
- data/lib/generators/railwatch/install/templates/initializer.rb.tt +4 -4
- data/lib/puma/plugin/railwatch.rb +48 -3
- data/lib/railwatch/configuration.rb +16 -1
- data/lib/railwatch/engine.rb +14 -3
- data/lib/railwatch/reporter.rb +52 -16
- data/lib/railwatch/transport/http.rb +4 -9
- data/lib/railwatch/version.rb +1 -1
- data/lib/railwatch.rb +23 -1
- data/lib/tasks/railwatch_tasks.rake +11 -4
- data/llms.txt +8 -4
- data/public/railwatch/assets/{app-layout-DDyQa72H.js → app-layout-2xAKndVD.js} +1 -1
- data/public/railwatch/assets/{app-wordmark-o9CODKP0.js → app-wordmark-Bt6XqURo.js} +1 -1
- data/public/railwatch/assets/{appearance-BwuCXabr.js → appearance-DyCfeh9o.js} +1 -1
- data/public/railwatch/assets/{arrow-up-C6PxDiY3.js → arrow-up-EmePsTKP.js} +1 -1
- data/public/railwatch/assets/{auth-layout-BRt8MGFD.js → auth-layout-BuwogWMZ.js} +1 -1
- data/public/railwatch/assets/{badge-CAxXV8za.js → badge-Dv7I-ebP.js} +1 -1
- data/public/railwatch/assets/{braces-DgomTCNf.js → braces-iZ7MEfYP.js} +1 -1
- data/public/railwatch/assets/{card-cAtqCxWl.js → card-DqdEEKcJ.js} +1 -1
- data/public/railwatch/assets/{chart-BBeBkkNa.js → chart-C4k7Ub9W.js} +1 -1
- data/public/railwatch/assets/{chart-hover-B1M9jc0y.js → chart-hover-2yZG3w2u.js} +1 -1
- data/public/railwatch/assets/{chart-panel-DUQTz_C8.js → chart-panel-CONkZcn9.js} +1 -1
- data/public/railwatch/assets/{checkbox-CmhMHWZO.js → checkbox-DKbThbxr.js} +1 -1
- data/public/railwatch/assets/{code-DESvxyTj.js → code-CYahYQHZ.js} +1 -1
- data/public/railwatch/assets/{copy-block-BkSU5832.js → copy-block-CbJum9s8.js} +1 -1
- data/public/railwatch/assets/{copy-id-D03GhN9F.js → copy-id-JmvLfkmx.js} +1 -1
- data/public/railwatch/assets/{cursor-load-more-CRyuMeQb.js → cursor-load-more-C1ylbqqL.js} +1 -1
- data/public/railwatch/assets/{data-table-BIlt7Rtm.js → data-table-CVKR2wkq.js} +1 -1
- data/public/railwatch/assets/{edit-DJ0D0wHN.js → edit-BXF5oNYA.js} +1 -1
- data/public/railwatch/assets/{edit-O0NSBWxo.js → edit-BwSEVrTG.js} +1 -1
- data/public/railwatch/assets/{edit-Bb6MKoe4.js → edit-D5BRDwLT.js} +1 -1
- data/public/railwatch/assets/{empty-state-C38il627.js → empty-state-Bn48Gplm.js} +1 -1
- data/public/railwatch/assets/{env-layout-REF7OM4q.js → env-layout-CKLL8Ds7.js} +1 -1
- data/public/railwatch/assets/{execution-path-FYLq1TwC.js → execution-path-DWovhgLi.js} +1 -1
- data/public/railwatch/assets/{filter-bar-CYog9Alp.js → filter-bar-xcbJC93z.js} +1 -1
- data/public/railwatch/assets/{flamegraph-DSs69foN.js → flamegraph-BNbNIuKT.js} +1 -1
- data/public/railwatch/assets/{frames-BUi2J5Mk.js → frames-Bk39xfJG.js} +1 -1
- data/public/railwatch/assets/{google-sign-in-button-BQiIKFdd.js → google-sign-in-button-B5as2ps1.js} +1 -1
- data/public/railwatch/assets/{index-QpTtwFwu.js → index-Atp1FTgX.js} +1 -1
- data/public/railwatch/assets/{index-C-PmdhXA.js → index-B-FXAo1w.js} +1 -1
- data/public/railwatch/assets/{index-DqTFTP8p.js → index-B0xgzHuN.js} +1 -1
- data/public/railwatch/assets/{index-so4lRrRq.js → index-BDVShWMs.js} +1 -1
- data/public/railwatch/assets/{index-C_upSl_k.js → index-BRvRkfob.js} +1 -1
- data/public/railwatch/assets/{index-CsoN51vW.js → index-BVWapDeB.js} +1 -1
- data/public/railwatch/assets/{index-CrZ3vHDL.js → index-Bd4YpOIO.js} +1 -1
- data/public/railwatch/assets/{index-BaR1U9An.js → index-BeQw69GM.js} +1 -1
- data/public/railwatch/assets/{index-Dh4IRLFI.js → index-BfitxVBv.js} +1 -1
- data/public/railwatch/assets/{index-umIAl-pL.js → index-C-EwH4lO.js} +1 -1
- data/public/railwatch/assets/{index-CICUIFHL.js → index-C8JIdb3_.js} +1 -1
- data/public/railwatch/assets/{index-CFFpnzIS.js → index-CBFCtJAR.js} +1 -1
- data/public/railwatch/assets/{index-tpz-OGUP.js → index-CDXlITyG.js} +1 -1
- data/public/railwatch/assets/{index-DW2CBbxU.js → index-COd9lT84.js} +1 -1
- data/public/railwatch/assets/{index-FhUaPPab.js → index-C_hQmVTP.js} +1 -1
- data/public/railwatch/assets/{index-JdCVBrw8.js → index-CcennT28.js} +1 -1
- data/public/railwatch/assets/{index-DrcKVG2f.js → index-CiaeS-n7.js} +1 -1
- data/public/railwatch/assets/{index-BoUBioBP.js → index-CrA7jBTK.js} +1 -1
- data/public/railwatch/assets/{index-C3A_9imx.js → index-D0mKj3Hg.js} +1 -1
- data/public/railwatch/assets/{index-BiiyMcA0.js → index-DV5MdmpY.js} +1 -1
- data/public/railwatch/assets/{index-CiPo4Gob.js → index-DaE-1xhx.js} +1 -1
- data/public/railwatch/assets/{index-CpkI015n.js → index-DdVg9LKX.js} +1 -1
- data/public/railwatch/assets/{index-DvjY3dPD.js → index-DeLtMWou.js} +1 -1
- data/public/railwatch/assets/{index-CFRLPs4J.js → index-DiQ4mqT_.js} +1 -1
- data/public/railwatch/assets/{index-DtHmuB9Q.js → index-LKgpCzoR.js} +1 -1
- data/public/railwatch/assets/{index-BeOh2t_S.js → index-S0NVaLj9.js} +1 -1
- data/public/railwatch/assets/{index-C7OtLq_3.js → index-Vj93BTR5.js} +1 -1
- data/public/railwatch/assets/{index-ZOGOB8SA.js → index-XjXcV-EO.js} +1 -1
- data/public/railwatch/assets/{index-ZSZg9rtq.js → index-Zkt4W_CM.js} +1 -1
- data/public/railwatch/assets/{index-8-hnAhOD.js → index-aPCZX-pW.js} +1 -1
- data/public/railwatch/assets/{index-DDI_Zx5V.js → index-cTzBycCg.js} +1 -1
- data/public/railwatch/assets/{index-sTYvcbkh.js → index-dpG-19ZN.js} +1 -1
- data/public/railwatch/assets/{index-r0tSIplE.js → index-gCvuyG1l.js} +1 -1
- data/public/railwatch/assets/{index-DSvlZVWG.js → index-o1xqGvLo.js} +1 -1
- data/public/railwatch/assets/{index-CGs4m_fa.js → index-w_skgMmK.js} +1 -1
- data/public/railwatch/assets/{inertia-DLew8ZNx.js → inertia-CqnzqPVD.js} +2 -2
- data/public/railwatch/assets/{input-error-cvM6_Jht.js → input-error-ByT28UdP.js} +1 -1
- data/public/railwatch/assets/{json-viewer-D922McGi.js → json-viewer-Bnu9RsJL.js} +1 -1
- data/public/railwatch/assets/{klass-CrwICqN8.js → klass-nLiBV-Az.js} +1 -1
- data/public/railwatch/assets/{label-GWl7I6sf.js → label-1nSzpSB6.js} +1 -1
- data/public/railwatch/assets/{layout-0ZAnD3zl.js → layout-D7lE1Doo.js} +1 -1
- data/public/railwatch/assets/{live-dot-D1n_BreY.js → live-dot-UdzOfzJb.js} +1 -1
- data/public/railwatch/assets/{nav-DPxr1NNC.js → nav-BRCgp2w3.js} +1 -1
- data/public/railwatch/assets/{new-DHAHDrN7.js → new-C60G72DR.js} +1 -1
- data/public/railwatch/assets/{new-D-ZzUK9a.js → new-COyDKOOb.js} +1 -1
- data/public/railwatch/assets/{new-DEVkYv-z.js → new-Cd9zPNJ8.js} +1 -1
- data/public/railwatch/assets/{new-Dz4lZf1L.js → new-DMJGKjmH.js} +1 -1
- data/public/railwatch/assets/{new-GMrRFurX.js → new-Omvf-c9s.js} +1 -1
- data/public/railwatch/assets/{new-Cdl6pqST.js → new-vjRzERL3.js} +1 -1
- data/public/railwatch/assets/onboarding-gUjJQc-6.js +1 -0
- data/public/railwatch/assets/{origin-identity-Bk9yHWZ1.js → origin-identity-M1YUPYS0.js} +1 -1
- data/public/railwatch/assets/{percentile-picker-DfSx9yJO.js → percentile-picker-BxIVblBw.js} +1 -1
- data/public/railwatch/assets/{relative-time-CjIjb8Lg.js → relative-time-DQ4kjowZ.js} +1 -1
- data/public/railwatch/assets/{release-health-4b3tivEf.js → release-health-Cb1tMOOO.js} +1 -1
- data/public/railwatch/assets/{route-C_5BUtHK.js → route-BJkoEm7r.js} +1 -1
- data/public/railwatch/assets/{segmented-BgbT3wZa.js → segmented-C4oWE73o.js} +1 -1
- data/public/railwatch/assets/{select-DmunxCKE.js → select-ocdQJdQo.js} +1 -1
- data/public/railwatch/assets/{separator-BXzEdZ_8.js → separator-Ct2UcZ2x.js} +1 -1
- data/public/railwatch/assets/{series-chart-Xf49v9cv.js → series-chart-D6upsrSn.js} +1 -1
- data/public/railwatch/assets/{show-DlRVS18-.js → show-B4bwI9x2.js} +1 -1
- data/public/railwatch/assets/{show-DnR1Dnjd.js → show-BAjfddEn.js} +1 -1
- data/public/railwatch/assets/{show-mU38uGTg.js → show-BLJOyLb_.js} +1 -1
- data/public/railwatch/assets/{show-DYskfl3-.js → show-BoVyMQS6.js} +1 -1
- data/public/railwatch/assets/{show-DXs4deaC.js → show-Bsu1rvnK.js} +1 -1
- data/public/railwatch/assets/{show-Dn-GwFZL.js → show-C6OiJ6Hr.js} +1 -1
- data/public/railwatch/assets/{show-CAl7xcex.js → show-C8TkpEgm.js} +1 -1
- data/public/railwatch/assets/{show-BLpWUHWD.js → show-CBFBiBY2.js} +1 -1
- data/public/railwatch/assets/{show-Y74rM0VT.js → show-CEhPVMMT.js} +1 -1
- data/public/railwatch/assets/{show-SHwZjXb7.js → show-CV-lG2b6.js} +1 -1
- data/public/railwatch/assets/{show-Dily73Xk.js → show-CX_pab9S.js} +1 -1
- data/public/railwatch/assets/{show-DcpTFiLi.js → show-D6lgzL6n.js} +1 -1
- data/public/railwatch/assets/{show-vQ4bndYD.js → show-DDVsv-uS.js} +1 -1
- data/public/railwatch/assets/{show-SvLOcPrx.js → show-Dwiq-8hA.js} +1 -1
- data/public/railwatch/assets/{show-B2zLAW83.js → show-eKazflRI.js} +1 -1
- data/public/railwatch/assets/{show-DSP9Cq_C.js → show-gtwKsTsl.js} +1 -1
- data/public/railwatch/assets/{show-DI8IhNUH.js → show-yEOrk6Pt.js} +1 -1
- data/public/railwatch/assets/{sort-header-Dcq9bzmo.js → sort-header-CPM6fhXv.js} +1 -1
- data/public/railwatch/assets/{sparkline-cell-BON3qQUB.js → sparkline-cell-CXCxXPWt.js} +1 -1
- data/public/railwatch/assets/{stat-DFEyFxkO.js → stat-BNEfE8X1.js} +1 -1
- data/public/railwatch/assets/{status-badge-BaUKP7Yo.js → status-badge-D0aylrM5.js} +1 -1
- data/public/railwatch/assets/{tenant-path-DPZPc985.js → tenant-path-C4aM_cO0.js} +1 -1
- data/public/railwatch/assets/{text-link-BO77t9Xk.js → text-link-CP4lqUi6.js} +1 -1
- data/public/railwatch/assets/{textarea-DTqrCiV0.js → textarea-DzJo2ds4.js} +1 -1
- data/public/railwatch/assets/{timeline-D5rJ0es2.js → timeline-BNcJ9PMc.js} +1 -1
- data/public/railwatch/assets/{transition-DMIrZVth.js → transition-BX2M1iaq.js} +1 -1
- data/public/railwatch/assets/{use-clipboard-ByoUGQqA.js → use-clipboard-C1ApSwzA.js} +1 -1
- data/public/railwatch/assets/{use-live-D7xKz2ma.js → use-live-DWwLg1Nj.js} +1 -1
- data/public/railwatch/manifest.json +1283 -1283
- metadata +115 -115
- data/public/railwatch/assets/onboarding-D1vwaHYT.js +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6da9b1e629335b1623cfc24e8b18c5a5d17bed076630768b9d947ff2d0b1a5c2
|
|
4
|
+
data.tar.gz: 827e8cc13fa4e1373bd6656c851e7fabbdfe2af60c37432c9feeb10801972367
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 741964c31992572e63aeabb9a601482d4a4facb6f895ac1c3fe362c9e0f493f7e7b7636b070bc0909b764e38514495f838e8dbda9de5fef8423cd978fc833281
|
|
7
|
+
data.tar.gz: 2eb78e494a27b97451023876f6e72e1cb73bf4aebacb002af148d674841a3f62348c2992cd131d16f7cc75a6308203d68da2738b1431791c4a15fd134b7d3fc4
|
data/AGENTS.md
CHANGED
|
@@ -14,10 +14,14 @@ render, exception, and span is a child of one, linked by
|
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
16
|
bundle add railwatch
|
|
17
|
-
bin/rails generate railwatch:install
|
|
17
|
+
bin/rails generate railwatch:install # embedded: dashboard at /railwatch
|
|
18
|
+
bin/rails generate railwatch:install --prompt-token --kamal-secrets # or: Railwatch Cloud
|
|
18
19
|
```
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
With no flags the install is embedded (see [Embedded mode](docs/embedded.md)):
|
|
22
|
+
telemetry stays in two SQLite databases the app owns and the dashboard is served
|
|
23
|
+
at `/railwatch`. `--cloud`, or any of `--prompt-token`, `--token-stdin`, `--url`,
|
|
24
|
+
`--kamal-secrets`, sends it to Railwatch Cloud instead. For the cloud install the generator writes `config/initializers/railwatch.rb`, mounts `Railwatch::Engine`
|
|
21
25
|
at `/railwatch`, adds the Kamal `post-deploy` hook and the Inertia browser client
|
|
22
26
|
where the app has them, requires `railwatch/rspec` (or `railwatch/minitest`) in the
|
|
23
27
|
test helper, and then runs `railwatch:doctor`. A prompted/stdin/environment token
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,124 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
<!-- Pull requests add their entry here. The version number and the date are
|
|
6
|
+
filled in by the release commit, which is also the only commit that
|
|
7
|
+
touches lib/railwatch/version.rb and Gemfile.lock. See CONTRIBUTING.md. -->
|
|
8
|
+
|
|
9
|
+
## 0.6.0 (2026-09-22)
|
|
10
|
+
|
|
11
|
+
- Embedded is now the installer's default. `bin/rails generate
|
|
12
|
+
railwatch:install` with no flags writes what `--local` used to: the
|
|
13
|
+
`railwatch` and `railwatch_telemetry` SQLite databases, `plugin
|
|
14
|
+
:railwatch` in `config/puma.rb`, and `c.transport = :local`, so two
|
|
15
|
+
commands give a working dashboard at `/railwatch` with no account and no
|
|
16
|
+
token. The cloud install is `--cloud`, and any option that only means
|
|
17
|
+
something there (`--prompt-token`, `--token-stdin`, `--url`,
|
|
18
|
+
`--kamal-secrets`) implies it, so existing cloud instructions keep
|
|
19
|
+
working. An exported `RAILWATCH_TOKEN` on its own does not pick the
|
|
20
|
+
cloud: a token in the shell is not a decision about where data goes.
|
|
21
|
+
`--local` is gone (it is the default); the gem's runtime default when no
|
|
22
|
+
initializer sets a transport is unchanged. The embedded next steps now
|
|
23
|
+
end with how to mirror to Railwatch Cloud (`c.export_enabled`).
|
|
24
|
+
|
|
25
|
+
- The embedded dashboard is open in development when HTTP Basic has no
|
|
26
|
+
credentials, so the first run needs no password step. Every other
|
|
27
|
+
environment is unchanged: closed, 401, until credentials exist. That
|
|
28
|
+
case now also logs a boot warning outside development and test (it was
|
|
29
|
+
only in the doctor), since a 401 on a deployed dashboard otherwise looks
|
|
30
|
+
like a broken install. Configured credentials apply in development too.
|
|
31
|
+
|
|
32
|
+
- Bound how long Puma waits for the embedded writer to stop. The plugin sent
|
|
33
|
+
the writer TERM and then called `Process.wait` on it, which has no timeout:
|
|
34
|
+
a writer that did not exit -- stuck in a SQLite write, on a full disk --
|
|
35
|
+
held Puma's shutdown open for as long as it stayed stuck. Measured against
|
|
36
|
+
a child that ignores TERM, the stop never returned (the harness gave up at
|
|
37
|
+
10s with the child still alive). Puma now waits `shutdown_timeout` (2s),
|
|
38
|
+
the same allowance it gives its own reporter, then KILLs the writer and
|
|
39
|
+
reaps it, so a wedged writer costs a bounded 2s and never leaves a zombie.
|
|
40
|
+
A writer killed mid-batch loses nothing -- the batch is retried by id
|
|
41
|
+
against the next writer -- which is why the bound is not the writer's own
|
|
42
|
+
worst-case drain (13s at the defaults): waiting for it would buy no data,
|
|
43
|
+
only exit time, and that time counts against the container's stop grace.
|
|
44
|
+
A writer that exits on TERM is let go the moment it does (measured
|
|
45
|
+
~50ms), and a pid the cluster has already reaped is still treated as gone.
|
|
46
|
+
|
|
47
|
+
- Make one HTTP attempt per delivery. `Transport::Http#deliver` retried a
|
|
48
|
+
raised error or a 5xx once on its own, inside a reporter that already owns
|
|
49
|
+
a retry ladder of eight attempts, so each rung cost two socket timeouts
|
|
50
|
+
and the effective attempt count was about sixteen. Against a receiver that
|
|
51
|
+
accepts connections and never answers, one delivery cost 6.0s; it now
|
|
52
|
+
costs 3.0s (one `read_timeout`), and one attempt, like `deliver_encoded`.
|
|
53
|
+
Response classification and `Retry-After` are unchanged: a 5xx or a raised
|
|
54
|
+
error still comes back retryable and the reporter still schedules it.
|
|
55
|
+
|
|
56
|
+
- Correct `docs/configuration.md` and `docs/troubleshooting.md`, which told
|
|
57
|
+
users to raise `buffer_size` under pressure. At the defaults the byte
|
|
58
|
+
ceiling (`buffer_bytes`, 16 MiB) fills at roughly 5,000 records on a
|
|
59
|
+
realistic mix, so the 10,000-record count is never reached and raising it
|
|
60
|
+
changes nothing. The setting stays; the advice now points at `buffer_bytes`.
|
|
61
|
+
|
|
62
|
+
- Say so when records are lost. `Railwatch.on_unrecoverable` fell back to
|
|
63
|
+
the debug log, so with no callback registered and `RAILWATCH_DEBUG` unset
|
|
64
|
+
a batch dropped after its retry ladder, one the receiver permanently
|
|
65
|
+
refused, or the records still unsent when `at_exit`'s bounded shutdown
|
|
66
|
+
ran out of time all vanished without a word. Since 0.3.7 that shutdown is
|
|
67
|
+
the only delivery a rake task or `rails runner` gets, so a cron job whose
|
|
68
|
+
exception never reached the platform looked exactly like one that had
|
|
69
|
+
nothing to report. Confirmed against a receiver that accepts and never
|
|
70
|
+
answers: the process left inside `shutdown_timeout` carrying three unsent
|
|
71
|
+
records and printed nothing.
|
|
72
|
+
|
|
73
|
+
A `Reporter::DeliveryError` -- raised only once the records are already
|
|
74
|
+
gone -- now prints one `[railwatch]` stderr line, and `warn_on_data_loss`
|
|
75
|
+
(`RAILWATCH_WARN_ON_DATA_LOSS`) defaults to **on**. Silence was the wrong
|
|
76
|
+
default: telemetry that disappears without a word looks exactly like
|
|
77
|
+
having nothing to report, which is the one failure an operator cannot
|
|
78
|
+
diagnose from the platform side, because the evidence is what went
|
|
79
|
+
missing. One line a deploy is the whole cost, and it only ever appears
|
|
80
|
+
when something was actually lost.
|
|
81
|
+
|
|
82
|
+
Both ways out are named in the line itself, so nobody has to find this
|
|
83
|
+
entry to stop it: a registered `on_unrecoverable` always wins, which is
|
|
84
|
+
how an app routes the loss somewhere better (`Rails.error.report`), and
|
|
85
|
+
`warn_on_data_loss = false` restores silence. Recovered internal errors (a
|
|
86
|
+
subscriber that raised, a flush that will be retried) stay debug-only
|
|
87
|
+
either way: the gem carried on and there is nothing for an operator to do.
|
|
88
|
+
|
|
89
|
+
- Report a lost batch outside the flush lock too. The fix above moved the
|
|
90
|
+
callback out of `@mutex`, the inner lock -- but `Reporter#flush` holds
|
|
91
|
+
`@flush_mutex` around the whole of `deliver_buffer`, and the give-up path,
|
|
92
|
+
the permanent-rejection path and the rescue all report from inside it. A
|
|
93
|
+
callback that asks this same reporter to flush (`Railwatch.flush` is public
|
|
94
|
+
and documented) hit the same non-reentrant `Mutex` one level out: the same
|
|
95
|
+
`ThreadError: deadlock; recursive locking`, rescued and hidden by
|
|
96
|
+
`notify_unrecoverable`, so the callback ran halfway and reported nothing.
|
|
97
|
+
The locked path now collects what it needs to report and `flush` hands it
|
|
98
|
+
over once the lock is released. Found by CodeRabbit on this pull request.
|
|
99
|
+
|
|
100
|
+
- Report a given-up batch after releasing the reporter lock, not under it.
|
|
101
|
+
`Reporter#retain` called `on_unrecoverable` inside `@mutex.synchronize`.
|
|
102
|
+
The documented callback is `Rails.error.report`, whose subscriber records
|
|
103
|
+
the error as an exception -- a write back into the same reporter, which
|
|
104
|
+
takes `@mutex` to arm its thread or request a flush. That was
|
|
105
|
+
`ThreadError: deadlock; recursive locking`, rescued and hidden by
|
|
106
|
+
`notify_unrecoverable`, so the callback died halfway and the loss it was
|
|
107
|
+
reporting was never seen. A callback that merely blocked held every
|
|
108
|
+
request thread's `write_now` and `shutdown` itself behind it for the
|
|
109
|
+
duration. `notify_unsent` and `delivery_rejected` already called out
|
|
110
|
+
unlocked; this was the one that did not.
|
|
111
|
+
|
|
112
|
+
- Investigated and left alone: `Reporter#shutdown` after `thread.join`.
|
|
113
|
+
The bookkeeping that follows the join (`pending_delivery`, the
|
|
114
|
+
once-only notify latch) takes `@mutex` for microseconds and never does
|
|
115
|
+
I/O -- measured 0.2ms over `shutdown_timeout` against a transport wedged
|
|
116
|
+
forever. The only thing that can extend it is the operator's own
|
|
117
|
+
callback, which runs once and is theirs to bound, as any `at_exit`
|
|
118
|
+
handler is. Wrapping it in `Timeout` would trade a visible cost for a
|
|
119
|
+
killed thread.
|
|
120
|
+
|
|
121
|
+
|
|
3
122
|
## 0.5.1 (2026-09-21)
|
|
4
123
|
|
|
5
124
|
Three small seams for a host that runs these models on its own routes and
|
data/README.md
CHANGED
|
@@ -6,25 +6,31 @@ outgoing HTTP, storage, views, and logs, and links them into one trace
|
|
|
6
6
|
per execution, for about half a millisecond per request plus tens of
|
|
7
7
|
microseconds per query, with zero writes to your database.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
[embedded mode](docs/embedded.md) serves the
|
|
9
|
+
By default all of it stays inside the app:
|
|
10
|
+
[embedded mode](docs/embedded.md) serves the full dashboard at
|
|
11
11
|
`/railwatch` out of two SQLite files your app owns, with no token, no
|
|
12
|
-
Node, no Redis and no job worker.
|
|
12
|
+
Node, no Redis and no job worker. Railwatch Cloud is optional, for
|
|
13
|
+
alerts that arrive when the app is down, MCP for your AI assistant, and
|
|
14
|
+
many apps and servers in one place.
|
|
13
15
|
|
|
14
16
|
## Install
|
|
15
17
|
|
|
16
18
|
```sh
|
|
17
|
-
bundle add railwatch
|
|
18
|
-
bin/rails generate railwatch:install
|
|
19
|
-
bin/rails railwatch:doctor # 3. check every piece is wired up after restart
|
|
19
|
+
bundle add railwatch # 1. add the public gem
|
|
20
|
+
bin/rails generate railwatch:install # 2. embedded: databases, Puma writer, dashboard at /railwatch
|
|
20
21
|
```
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
Restart and open `/railwatch`. It is open in development; before you
|
|
24
|
+
deploy, give it a password with
|
|
25
|
+
`RAILS_ENV=production bin/rails railwatch:authentication:configure`
|
|
26
|
+
(it answers 401 in production until you do).
|
|
27
|
+
|
|
28
|
+
Or send everything to Railwatch Cloud instead:
|
|
24
29
|
|
|
25
30
|
```sh
|
|
26
31
|
bundle add railwatch
|
|
27
|
-
bin/rails generate railwatch:install --
|
|
32
|
+
bin/rails generate railwatch:install --prompt-token # hidden token input plus app wiring
|
|
33
|
+
bin/rails railwatch:doctor # check every piece is wired up after restart
|
|
28
34
|
```
|
|
29
35
|
|
|
30
36
|
Getting the token, the generator's flags, and deploying with Kamal,
|
|
@@ -73,7 +79,7 @@ expect { get "/widgets" }.not_to have_railwatch_n_plus_one
|
|
|
73
79
|
|
|
74
80
|
## Embedded mode
|
|
75
81
|
|
|
76
|
-
|
|
82
|
+
The default install keeps everything inside the application. Telemetry goes to two
|
|
77
83
|
SQLite files it owns -- `railwatch` for issues, comments and saved views,
|
|
78
84
|
`railwatch_telemetry` for what the app reports -- and the dashboard is
|
|
79
85
|
served at `/railwatch` from a bundle shipped inside the gem. Nothing
|
|
@@ -86,9 +92,11 @@ runs the maintenance clock too, so exception grouping, rollups, retention
|
|
|
86
92
|
and threshold scans happen without a queue.
|
|
87
93
|
|
|
88
94
|
That dashboard reads every query, log line and exception the app
|
|
89
|
-
produced, so it is closed by default the way
|
|
90
|
-
HTTP Basic is on with no credentials, and every
|
|
91
|
-
|
|
95
|
+
produced, so outside development it is closed by default the way
|
|
96
|
+
Mission Control Jobs is: HTTP Basic is on with no credentials, and every
|
|
97
|
+
request is 401 until you set them with
|
|
98
|
+
`bin/rails railwatch:authentication:configure`. In development, with no
|
|
99
|
+
credentials set, it is open. Apps that
|
|
92
100
|
would rather use their own session hand it a `dashboard_user` resolver
|
|
93
101
|
instead. [Embedded mode](docs/embedded.md) covers all of it, including
|
|
94
102
|
upgrades and what it costs to store.
|
|
@@ -47,6 +47,7 @@ module Railwatch
|
|
|
47
47
|
def authenticate_by_http_basic
|
|
48
48
|
config = Railwatch.config
|
|
49
49
|
return unless config.http_basic_auth_enabled
|
|
50
|
+
return if config.http_basic_auth_waived?
|
|
50
51
|
|
|
51
52
|
if config.http_basic_auth_configured?
|
|
52
53
|
http_basic_authenticate_or_request_with(name: config.http_basic_auth_user, password: config.http_basic_auth_password,
|
|
@@ -15,7 +15,7 @@ module Railwatch
|
|
|
15
15
|
# No `railwatch` entry in this environment's database.yml, or an entry
|
|
16
16
|
# whose adapter gem is not in the bundle yet (LoadError). A cloud-transport
|
|
17
17
|
# app has none and still eager-loads this class in production, and so
|
|
18
|
-
# does the
|
|
18
|
+
# does the installer's own boot, before it has written the
|
|
19
19
|
# entry -- so loading must not raise. Using it must, though: without
|
|
20
20
|
# connects_to this class would inherit ActiveRecord::Base's PRIMARY
|
|
21
21
|
# connection, and its tables are unprefixed, so a query would read and
|
|
@@ -25,7 +25,7 @@ module Railwatch
|
|
|
25
25
|
def self.connection_pool
|
|
26
26
|
raise Railwatch::DatabaseNotConfigured,
|
|
27
27
|
"the `railwatch` database (issues, comments, saved views and deploys) is not configured for the " \
|
|
28
|
-
"#{Rails.env} environment; run `bin/rails generate railwatch:install
|
|
28
|
+
"#{Rails.env} environment; run `bin/rails generate railwatch:install` " \
|
|
29
29
|
"or add it to config/database.yml (docs/embedded.md)"
|
|
30
30
|
end
|
|
31
31
|
end
|
|
@@ -18,7 +18,7 @@ module Railwatch
|
|
|
18
18
|
# No `railwatch_telemetry` entry in this environment's database.yml, or an entry
|
|
19
19
|
# whose adapter gem is not in the bundle yet (LoadError). A cloud-transport
|
|
20
20
|
# app has none and still eager-loads this class in production, and so
|
|
21
|
-
# does the
|
|
21
|
+
# does the installer's own boot, before it has written the
|
|
22
22
|
# entry -- so loading must not raise. Using it must, though: without
|
|
23
23
|
# connects_to this class would inherit ActiveRecord::Base's PRIMARY
|
|
24
24
|
# connection, and its tables are unprefixed, so a query would read and
|
|
@@ -28,7 +28,7 @@ module Railwatch
|
|
|
28
28
|
def self.connection_pool
|
|
29
29
|
raise Railwatch::DatabaseNotConfigured,
|
|
30
30
|
"the `railwatch_telemetry` database (everything the app reports) is not configured for the " \
|
|
31
|
-
"#{Rails.env} environment; run `bin/rails generate railwatch:install
|
|
31
|
+
"#{Rails.env} environment; run `bin/rails generate railwatch:install` " \
|
|
32
32
|
"or add it to config/database.yml (docs/embedded.md)"
|
|
33
33
|
end
|
|
34
34
|
end
|
data/docs/configuration.md
CHANGED
|
@@ -385,7 +385,7 @@ the app database.
|
|
|
385
385
|
|
|
386
386
|
| Attribute | Env var | Default | Meaning |
|
|
387
387
|
|---|---|---|---|
|
|
388
|
-
| `buffer_size` | `RAILWATCH_BUFFER_SIZE` | `10000` | Max buffered records (`Railwatch::Buffer`). Oldest is dropped (and counted) when full — never blocks the request thread.
|
|
388
|
+
| `buffer_size` | `RAILWATCH_BUFFER_SIZE` | `10000` | Max buffered records (`Railwatch::Buffer`). Oldest is dropped (and counted) when full — never blocks the request thread. In practice `buffer_bytes` fills first: on a realistic mix of records, 16 MiB holds about 5,000 of them, so this count is never reached and raising it changes nothing. Tune `buffer_bytes` instead. Do not lower this below `Execution::MAX_RECORDS` (10,000): a kept execution's whole tree is written here at once when it ends, and a queue smaller than the tree drops the tree's own oldest records first. |
|
|
389
389
|
| `buffer_bytes` | `RAILWATCH_BUFFER_BYTES` | `16777216` (16 MiB) | Estimated payload memory the reporter queue may hold. A record count alone does not bound memory: 10,000 records is a few megabytes of ordinary telemetry, or a gigabyte of captured attachments. Oldest records are dropped (and counted) under byte pressure, same as under count pressure. |
|
|
390
390
|
| `execution_buffer_bytes` | `RAILWATCH_EXECUTION_BUFFER_BYTES` | `8388608` (8 MiB) | The same ceiling for one execution's buffered tree, before it finishes. A normal execution keeps its earliest records; a failure-context ring keeps its latest. |
|
|
391
391
|
| `batch_bytes` | `RAILWATCH_BATCH_BYTES` | `8388608` (8 MiB) | Uncompressed NDJSON bytes in one ingest request. A queue holding more than this is delivered as several batches — the tail is kept for the next flush, not dropped. |
|
|
@@ -399,13 +399,13 @@ the app database.
|
|
|
399
399
|
|
|
400
400
|
Delivery is `Railwatch::Transport::Http`, in
|
|
401
401
|
`lib/railwatch/transport/http.rb`: a gzip NDJSON POST to
|
|
402
|
-
`{ingest_url}/ingest`,
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
the platform can return the first committed
|
|
408
|
-
payload twice. Records written while a request is in flight collect in a
|
|
402
|
+
`{ingest_url}/ingest`, one attempt per delivery. If it raises, or ingest
|
|
403
|
+
returns 402, 408, 429, or a 5xx, the immutable batch and its prior drop
|
|
404
|
+
count are retained for retry; the transport does not retry on its own,
|
|
405
|
+
so each rung of the reporter's ladder costs one timeout, not two. Every
|
|
406
|
+
newly formed batch gets an `X-Railwatch-Batch-Id` UUID. It is reused for
|
|
407
|
+
every reporter retry, so the platform can return the first committed
|
|
408
|
+
result without inserting the payload twice. Records written while a request is in flight collect in a
|
|
409
409
|
separate bounded buffer, so they never change the retained request's
|
|
410
410
|
identity. At most one retained batch plus one live buffer are held in
|
|
411
411
|
memory. The reporter retries with jittered exponential backoff, from one
|
|
@@ -827,9 +827,25 @@ Railwatch.on_unrecoverable { |error| Rails.error.report(error, handled: true) }
|
|
|
827
827
|
Called whenever Railwatch rescues one of its own internal errors, ingest
|
|
828
828
|
permanently rejects a batch, or shutdown expires with retained records
|
|
829
829
|
that could not be sent. Retryable delivery failures stay buffered and do
|
|
830
|
-
not fire the callback on every attempt. With no callback registered,
|
|
831
|
-
falls back to `Railwatch.debug
|
|
832
|
-
`RAILWATCH_DEBUG
|
|
830
|
+
not fire the callback on every attempt. With no callback registered, a
|
|
831
|
+
recovered internal error falls back to `Railwatch.debug`, gated on
|
|
832
|
+
`RAILWATCH_DEBUG` — the gem carried on and there is nothing to do about it.
|
|
833
|
+
|
|
834
|
+
**Lost records are different, and are reported by default.** A batch
|
|
835
|
+
dropped after its retry ladder, one the receiver permanently refused, or
|
|
836
|
+
records still unsent when the bounded shutdown ran out of time each print
|
|
837
|
+
one `[railwatch]` stderr line. Telemetry that vanishes silently looks
|
|
838
|
+
exactly like having nothing to report, and a short-lived process — a rake
|
|
839
|
+
task, a `rails runner`, a cron job — gets one bounded shutdown and no
|
|
840
|
+
second chance to mention it.
|
|
841
|
+
|
|
842
|
+
Two ways out, and the line names both. A registered `on_unrecoverable`
|
|
843
|
+
always wins, which is how an app routes the loss somewhere better
|
|
844
|
+
(`Rails.error.report`). Or set `warn_on_data_loss = false`
|
|
845
|
+
(`RAILWATCH_WARN_ON_DATA_LOSS=false`) and the gem goes back to saying
|
|
846
|
+
nothing.
|
|
847
|
+
|
|
848
|
+
Either way it is stderr, never `Rails.logger`, so gem-internal failures can
|
|
833
849
|
never themselves become `log` records.
|
|
834
850
|
|
|
835
851
|
## Faraday
|
|
@@ -872,7 +888,7 @@ c.transport = :local # RAILWATCH_TRANSPORT; default "http"
|
|
|
872
888
|
c.issue_prefix = "SHOP" # RAILWATCH_ISSUE_PREFIX; default from the app name
|
|
873
889
|
c.repository_url = "..." # RAILWATCH_REPOSITORY_URL
|
|
874
890
|
c.retention_days = 7 # RAILWATCH_RETENTION_DAYS
|
|
875
|
-
c.http_basic_auth_enabled = true # RAILWATCH_HTTP_BASIC_AUTH_ENABLED; on and closed until credentials exist
|
|
891
|
+
c.http_basic_auth_enabled = true # RAILWATCH_HTTP_BASIC_AUTH_ENABLED; on, and closed outside development until credentials exist
|
|
876
892
|
c.http_basic_auth_user = "ops" # RAILWATCH_HTTP_BASIC_AUTH_USER, or credentials railwatch.http_basic_auth_user
|
|
877
893
|
c.http_basic_auth_password = "..." # RAILWATCH_HTTP_BASIC_AUTH_PASSWORD, or credentials railwatch.http_basic_auth_password
|
|
878
894
|
c.base_controller_class = "AdminController" # RAILWATCH_BASE_CONTROLLER_CLASS; default ActionController::Base
|
|
@@ -884,7 +900,7 @@ With `transport = :local` the reporter writes each batch into the app's
|
|
|
884
900
|
own `railwatch_telemetry` database instead of POSTing it, and the engine
|
|
885
901
|
serves the dashboard at its mount. `enabled?` no longer needs a token.
|
|
886
902
|
The others only matter in that mode. The dashboard is behind HTTP Basic
|
|
887
|
-
by default and answers 401 until `bin/rails
|
|
903
|
+
by default and, outside development, answers 401 until `bin/rails
|
|
888
904
|
railwatch:authentication:configure` has written credentials; a host with
|
|
889
905
|
its own admin auth turns Basic off and sets `base_controller_class` or a
|
|
890
906
|
routes constraint. Full walkthrough: [Embedded mode](embedded.md).
|
data/docs/embedded.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Embedded mode: the dashboard inside your app
|
|
2
2
|
|
|
3
|
-
Railwatch
|
|
4
|
-
full dashboard at `/railwatch`, with no token and no cloud. The gem's
|
|
3
|
+
This is the default. Railwatch keeps every record in your own application
|
|
4
|
+
and serves the full dashboard at `/railwatch`, with no token and no cloud. The gem's
|
|
5
5
|
reporter, buffer and sampling are the same; the only difference is
|
|
6
6
|
where a batch ends up. In embedded mode it is written straight into a
|
|
7
7
|
SQLite database your app owns, and the dashboard reads it back from
|
|
@@ -25,8 +25,9 @@ up:
|
|
|
25
25
|
| **Cloud** | Railwatch Cloud | the hosted one |
|
|
26
26
|
| **Both** | your app's files, *and* Railwatch Cloud | either |
|
|
27
27
|
|
|
28
|
-
Embedded is `c.transport = :local`, which is what
|
|
29
|
-
|
|
28
|
+
Embedded is `c.transport = :local`, which is what the installer writes
|
|
29
|
+
unless you ask it for the cloud (`--cloud`, or any token or URL option).
|
|
30
|
+
Cloud is the gem's default when no initializer says otherwise. "Both" is embedded plus one more line:
|
|
30
31
|
|
|
31
32
|
```ruby
|
|
32
33
|
c.export_enabled = true # or RAILWATCH_EXPORT_ENABLED=true
|
|
@@ -59,10 +60,11 @@ documents: `id`, `slug`, `name`, `with_telemetry`, and the display attributes.
|
|
|
59
60
|
|
|
60
61
|
```sh
|
|
61
62
|
bundle add railwatch
|
|
62
|
-
bin/rails generate railwatch:install
|
|
63
|
+
bin/rails generate railwatch:install
|
|
63
64
|
```
|
|
64
65
|
|
|
65
|
-
Restart the app and open `/railwatch
|
|
66
|
+
Restart the app and open `/railwatch`; in development it is open with no
|
|
67
|
+
password (see [Authentication](#authentication) for production). Then `bin/rails railwatch:doctor`
|
|
66
68
|
checks the wiring. The generator creates and migrates both databases
|
|
67
69
|
itself; `bin/rails db:prepare`, which a deploy already runs, migrates
|
|
68
70
|
them after every gem update.
|
|
@@ -83,7 +85,7 @@ On a PostgreSQL or MySQL app that means the install is two commands
|
|
|
83
85
|
rather than one, because SQLite's adapter gem will not be in your bundle:
|
|
84
86
|
|
|
85
87
|
```sh
|
|
86
|
-
bin/rails generate railwatch:install
|
|
88
|
+
bin/rails generate railwatch:install # adds gem "sqlite3", writes the config
|
|
87
89
|
bundle install
|
|
88
90
|
bin/rails db:prepare # creates the two SQLite files
|
|
89
91
|
```
|
|
@@ -92,7 +94,7 @@ Verified end to end on both. On a PostgreSQL app and on a MySQL app, the
|
|
|
92
94
|
application's own four databases stay where they were, Railwatch's two are
|
|
93
95
|
files under `storage/`, and neither server gains a single Railwatch table.
|
|
94
96
|
|
|
95
|
-
What
|
|
97
|
+
What the embedded install writes, on top of what every install writes:
|
|
96
98
|
|
|
97
99
|
- `config/initializers/railwatch.rb` with `c.transport = :local` and the
|
|
98
100
|
dashboard's own paths excluded from request capture.
|
|
@@ -140,15 +142,22 @@ and the MCP server.
|
|
|
140
142
|
The dashboard shows every query, log line and exception your app
|
|
141
143
|
produced, so it works the way Mission Control Jobs does: **HTTP Basic
|
|
142
144
|
authentication is on and closed by default**. With no credentials
|
|
143
|
-
configured every dashboard request is 401,
|
|
144
|
-
so. Set them with
|
|
145
|
+
configured every dashboard request is 401, the app logs a warning at
|
|
146
|
+
boot, and `railwatch:doctor` says so. Set them with
|
|
145
147
|
|
|
146
148
|
```sh
|
|
147
149
|
bin/rails railwatch:authentication:configure
|
|
148
150
|
RAILS_ENV=production bin/rails railwatch:authentication:configure
|
|
149
151
|
```
|
|
150
152
|
|
|
151
|
-
|
|
153
|
+
The one exception is development. There, with Basic on and no
|
|
154
|
+
credentials set, the dashboard is open, so a first run is the install and
|
|
155
|
+
a page rather than a password step first; Rails already shows full error
|
|
156
|
+
pages in development for the same reason. Set credentials there too and
|
|
157
|
+
development asks for them like everywhere else. Test, staging and
|
|
158
|
+
production are closed until you do.
|
|
159
|
+
|
|
160
|
+
`railwatch:authentication:configure` writes them to that environment's Rails credentials:
|
|
152
161
|
|
|
153
162
|
```yml
|
|
154
163
|
railwatch:
|
|
@@ -262,7 +271,7 @@ authorisation rule as well as a label.
|
|
|
262
271
|
## The writer process
|
|
263
272
|
|
|
264
273
|
Puma forks one Railwatch writer from its master when `config/puma.rb`
|
|
265
|
-
carries the plugin (
|
|
274
|
+
carries the plugin (the embedded install adds it):
|
|
266
275
|
|
|
267
276
|
```ruby
|
|
268
277
|
plugin :railwatch if defined?(Railwatch)
|
|
@@ -319,6 +328,17 @@ through `Railwatch.on_unrecoverable`, so loss is never silent. A Puma
|
|
|
319
328
|
phased restart stops the writer and starts a fresh one once the new
|
|
320
329
|
workers are up.
|
|
321
330
|
|
|
331
|
+
Stopping the writer is bounded. Puma sends it TERM and waits up to
|
|
332
|
+
`c.shutdown_timeout` (2 seconds) for it to exit, the same allowance it
|
|
333
|
+
gives its own reporter, then kills it. A writer killed mid-batch loses
|
|
334
|
+
nothing: the transaction rolls back and the worker retries that batch by
|
|
335
|
+
id against the next writer, so waiting longer for its drain would buy no
|
|
336
|
+
data. The bound is what keeps Puma's exit short when the writer is wedged
|
|
337
|
+
in a SQLite write or on a full disk. It counts against the container's
|
|
338
|
+
stop grace (Docker's default is 10 seconds; Kamal's `stop_timeout` sets
|
|
339
|
+
it), and `RAILWATCH_SHUTDOWN_TIMEOUT` raises it for an app whose grace
|
|
340
|
+
allows more.
|
|
341
|
+
|
|
322
342
|
## Maintenance
|
|
323
343
|
|
|
324
344
|
Railwatch needs no job worker and nothing in `config/recurring.yml`.
|
data/docs/faq.md
CHANGED
|
@@ -182,10 +182,10 @@ being silent.
|
|
|
182
182
|
A queue holding more than one batch is delivered as several batches: the
|
|
183
183
|
tail is put back for the next flush rather than dropped.
|
|
184
184
|
|
|
185
|
-
A background thread drains the buffer and POSTs
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
185
|
+
A background thread drains the buffer and POSTs, one attempt per delivery.
|
|
186
|
+
If it fails, the batch and its drop counter go back into the bounded
|
|
187
|
+
buffer; a raised network error, **402**, **408**, **429**, and all **5xx**
|
|
188
|
+
responses are retained the same way. So is a **2xx
|
|
189
189
|
that cannot acknowledge the batch** — a proxy's HTML sign-in page, malformed
|
|
190
190
|
JSON, or `accepted`/`rejected` counts that do not cover what was sent — which
|
|
191
191
|
would otherwise be a silent drop. The reporter
|
|
@@ -217,7 +217,8 @@ already appear on its own ingest batch. Those are visible under
|
|
|
217
217
|
On shutdown, `at_exit` gives the thread `c.shutdown_timeout` (2 seconds) to
|
|
218
218
|
attempt retained records immediately and retry within the remaining time.
|
|
219
219
|
If the deadline expires, the records stay retained and their count is sent
|
|
220
|
-
to `Railwatch.on_unrecoverable` (or
|
|
220
|
+
to `Railwatch.on_unrecoverable` (or, with no callback, to `Railwatch.debug`, and
|
|
221
|
+
to stderr by default, which `warn_on_data_loss = false` turns off). This is an
|
|
221
222
|
in-memory buffer, not an on-disk spool: a hard kill, or exiting after that
|
|
222
223
|
deadline, cannot carry those records into the next process. Railwatch never
|
|
223
224
|
uses `Rails.logger` for its own failures, which would turn them into `log`
|
data/docs/getting-started.md
CHANGED
|
@@ -4,10 +4,10 @@ Five minutes from `bundle add` to a request on the dashboard, on a
|
|
|
4
4
|
Rails 8 app. Everything below is the gem's own generator and rake tasks;
|
|
5
5
|
nothing else has to be wired by hand.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
no token and no cloud
|
|
9
|
-
|
|
10
|
-
the
|
|
7
|
+
The installer's default is embedded: the dashboard inside the app at
|
|
8
|
+
`/railwatch`, no token and no cloud; see [Embedded mode](embedded.md).
|
|
9
|
+
This page is the cloud install, which is the same generator with
|
|
10
|
+
`--cloud` or any of the token options below.
|
|
11
11
|
|
|
12
12
|
## 1. Add the gem
|
|
13
13
|
|
|
@@ -21,7 +21,7 @@ The gem, its Ruby namespace, and its require path share one name:
|
|
|
21
21
|
## 2. Run the installer
|
|
22
22
|
|
|
23
23
|
```sh
|
|
24
|
-
bin/rails generate railwatch:install
|
|
24
|
+
bin/rails generate railwatch:install --cloud
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
With the token already in hand, let the generator read it without placing the
|
|
@@ -36,6 +36,10 @@ bin/rails generate railwatch:install \
|
|
|
36
36
|
|
|
37
37
|
- `--prompt-token` reads without echo. `--token-stdin` is available for a
|
|
38
38
|
secret-manager pipe; an already exported `RAILWATCH_TOKEN` is also detected.
|
|
39
|
+
- Any of `--prompt-token`, `--token-stdin`, `--url=` and `--kamal-secrets`
|
|
40
|
+
means the cloud, so none of them needs `--cloud` as well. An exported
|
|
41
|
+
`RAILWATCH_TOKEN` on its own does not: without one of these flags the
|
|
42
|
+
install is embedded.
|
|
39
43
|
- A token is written to `.env` only when Git confirms that `.env` is ignored.
|
|
40
44
|
A tracked or non-ignored dotenv file is refused; use Rails credentials, a
|
|
41
45
|
deployment secret manager, or add `.env` to `.gitignore` first. Token values
|
data/docs/troubleshooting.md
CHANGED
|
@@ -215,14 +215,18 @@ can't be made until it ends.
|
|
|
215
215
|
(10,000). Past that, records are dropped and counted. The count is
|
|
216
216
|
added to the reporter's drop counter so the loss is visible on the
|
|
217
217
|
platform rather than silent.
|
|
218
|
-
-
|
|
219
|
-
|
|
220
|
-
Oldest-dropped-first, also
|
|
221
|
-
|
|
222
|
-
|
|
218
|
+
- The process-wide queue between the app and the reporter thread is
|
|
219
|
+
bounded by `c.buffer_bytes` (default 16 MiB) and by `c.buffer_size`
|
|
220
|
+
(default 10,000, the same as `MAX_RECORDS`). Oldest-dropped-first, also
|
|
221
|
+
counted. The byte ceiling is the one that fills: on a realistic mix of
|
|
222
|
+
records, 16 MiB holds about 5,000 of them, so raising `buffer_size`
|
|
223
|
+
changes nothing. Do not set it below `MAX_RECORDS`, though: an
|
|
224
|
+
execution's tree is written to the queue in one go when it ends, so a
|
|
225
|
+
tree larger than the queue loses its own first records. Typically
|
|
223
226
|
those are the outgoing requests a long job made before it started
|
|
224
227
|
writing. Keeping far more executions than before means far more
|
|
225
|
-
records arriving at this queue. Raise
|
|
228
|
+
records arriving at this queue. Raise `buffer_bytes`, or lower what
|
|
229
|
+
you keep.
|
|
226
230
|
- `c.profile_slow_ms` compounds it. It profiles every tail-buffering
|
|
227
231
|
execution from its first line and throws away the fast ones, so the
|
|
228
232
|
profiler's stack table is held alongside the record buffer.
|