railwatch 0.5.0 → 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 +133 -0
- data/README.md +21 -13
- data/app/controllers/railwatch/dashboard_controller.rb +1 -0
- data/app/controllers/railwatch/requests_controller.rb +1 -19
- data/app/models/railwatch/application_record.rb +2 -2
- data/app/models/railwatch/execution_presenter.rb +1 -1
- data/app/models/railwatch/filter_query.rb +20 -0
- data/app/models/railwatch/saved_view.rb +1 -1
- 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 +35 -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 +116 -116
- data/public/railwatch/assets/onboarding-D1vwaHYT.js +0 -1
- /data/db/railwatch_telemetry_migrate/{20260919000000_create_export_queue.rb → 20260919000100_create_export_queue.rb} +0 -0
|
@@ -9,10 +9,11 @@ module Railwatch
|
|
|
9
9
|
source_root File.expand_path("templates", __dir__)
|
|
10
10
|
|
|
11
11
|
desc "Creates config/initializers/railwatch.rb, a Kamal post-deploy hook, the browser client, and wires the test helpers. " \
|
|
12
|
-
"
|
|
12
|
+
"By default telemetry stays in this app, in two SQLite databases, with the dashboard at /railwatch. " \
|
|
13
|
+
"--cloud (or any token or URL option) sends it to Railwatch Cloud instead."
|
|
13
14
|
|
|
14
|
-
class_option :
|
|
15
|
-
desc: "
|
|
15
|
+
class_option :cloud, type: :boolean, default: false,
|
|
16
|
+
desc: "Send telemetry to Railwatch Cloud instead of keeping it in this app. Implied by --prompt-token, --token-stdin, --url and --kamal-secrets."
|
|
16
17
|
class_option :prompt_token, type: :boolean, default: false,
|
|
17
18
|
desc: "Prompt for the ingest token without echoing it."
|
|
18
19
|
class_option :token_stdin, type: :boolean, default: false,
|
|
@@ -67,9 +68,9 @@ module Railwatch
|
|
|
67
68
|
# database is, so an app on PostgreSQL or MySQL needs the adapter gem
|
|
68
69
|
# added before those files can be created.
|
|
69
70
|
def ensure_sqlite3_gem
|
|
70
|
-
return unless
|
|
71
|
+
return unless local?
|
|
71
72
|
return if Gem.loaded_specs.key?("sqlite3")
|
|
72
|
-
return say("
|
|
73
|
+
return say("Embedded mode needs the sqlite3 gem for its two databases; add `gem \"sqlite3\"` and re-run.", :yellow) unless File.exist?("Gemfile")
|
|
73
74
|
|
|
74
75
|
contents = File.read("Gemfile")
|
|
75
76
|
unless contents.match?(/^\s*gem ["']sqlite3["']/)
|
|
@@ -89,9 +90,9 @@ module Railwatch
|
|
|
89
90
|
# creates the tables now and migrates them after every gem update.
|
|
90
91
|
# Nothing is copied into the app.
|
|
91
92
|
def configure_local_databases
|
|
92
|
-
return unless
|
|
93
|
+
return unless local?
|
|
93
94
|
|
|
94
|
-
return say("
|
|
95
|
+
return say("No config/database.yml found; add railwatch and railwatch_telemetry databases yourself (docs/embedded.md).", :yellow) unless File.exist?("config/database.yml")
|
|
95
96
|
|
|
96
97
|
contents = File.read("config/database.yml")
|
|
97
98
|
updated = self.class.database_yml_with_railwatch(contents)
|
|
@@ -103,8 +104,8 @@ module Railwatch
|
|
|
103
104
|
# The writer process: one per Puma master, forked by the gem's Puma
|
|
104
105
|
# plugin, so batches are mapped and written outside the web workers.
|
|
105
106
|
def configure_local_writer
|
|
106
|
-
return unless
|
|
107
|
-
return say("
|
|
107
|
+
return unless local?
|
|
108
|
+
return say("No config/puma.rb found; add `plugin :railwatch` to your Puma config yourself (docs/embedded.md).", :yellow) unless File.exist?("config/puma.rb")
|
|
108
109
|
|
|
109
110
|
contents = File.read("config/puma.rb")
|
|
110
111
|
updated = self.class.puma_rb_with_railwatch(contents)
|
|
@@ -166,7 +167,7 @@ module Railwatch
|
|
|
166
167
|
# A token lands in .env only when Git confirms the file is ignored.
|
|
167
168
|
# URLs are not secret and can still be written to a tracked dotenv file.
|
|
168
169
|
def write_env
|
|
169
|
-
return if
|
|
170
|
+
return if local?
|
|
170
171
|
|
|
171
172
|
token = resolved_token
|
|
172
173
|
vars = { TOKEN_VAR => token, URL_VAR => options[:url] }.compact
|
|
@@ -219,7 +220,7 @@ module Railwatch
|
|
|
219
220
|
# the same prepare a deploy runs, for both databases only. The host's
|
|
220
221
|
# own databases are not touched, and a schema file is never written.
|
|
221
222
|
def prepare_local_databases
|
|
222
|
-
return unless
|
|
223
|
+
return unless local?
|
|
223
224
|
return unless File.exist?("config/database.yml")
|
|
224
225
|
return if @needs_bundle
|
|
225
226
|
return unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
@@ -242,21 +243,25 @@ module Railwatch
|
|
|
242
243
|
end
|
|
243
244
|
|
|
244
245
|
def show_next_steps
|
|
245
|
-
if
|
|
246
|
+
if local?
|
|
246
247
|
say <<~STEPS, :green
|
|
247
248
|
|
|
248
249
|
Next steps
|
|
249
|
-
1.
|
|
250
|
-
|
|
250
|
+
1. Restart the app and open /railwatch. In development it is open.
|
|
251
|
+
2. Before production, give it a password (it is closed there until
|
|
252
|
+
you do): RAILS_ENV=production bin/rails railwatch:authentication:configure
|
|
251
253
|
Using your own admin auth instead? See docs/embedded.md,
|
|
252
254
|
Authentication (base_controller_class or a routes constraint).
|
|
253
|
-
2. Restart the app and open /railwatch.
|
|
254
255
|
3. #{@prepared ? "Nothing else to run. Both databases were created just now and" : "Create the two databases: bin/rails db:prepare\n Then"}
|
|
255
256
|
`bin/rails db:prepare` (which a deploy already runs) migrates
|
|
256
257
|
them after every gem update. With `plugin :railwatch` in
|
|
257
258
|
config/puma.rb Puma forks one Railwatch writer process that
|
|
258
259
|
writes every batch and runs the maintenance clock, so no web
|
|
259
260
|
process ever holds the telemetry database. No job worker.
|
|
261
|
+
4. Optional: mirror to Railwatch Cloud for alerts that still
|
|
262
|
+
arrive when this app is down, MCP for your AI assistant, and
|
|
263
|
+
every app in one place. Set RAILWATCH_TOKEN and
|
|
264
|
+
c.export_enabled = true (docs/embedded.md).
|
|
260
265
|
STEPS
|
|
261
266
|
return
|
|
262
267
|
end
|
|
@@ -287,7 +292,7 @@ module Railwatch
|
|
|
287
292
|
# This process read its configuration before the initializer was
|
|
288
293
|
# written, so in local mode the doctor would report an http transport
|
|
289
294
|
# with no token. The databases it would check were prepared above.
|
|
290
|
-
return if
|
|
295
|
+
return if local?
|
|
291
296
|
return unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
292
297
|
|
|
293
298
|
say "\nbin/rails railwatch:doctor", :green
|
|
@@ -455,6 +460,16 @@ module Railwatch
|
|
|
455
460
|
|
|
456
461
|
private
|
|
457
462
|
|
|
463
|
+
# Embedded unless the invocation asks for the cloud: --cloud itself, or
|
|
464
|
+
# an option that only means something there. A RAILWATCH_TOKEN already
|
|
465
|
+
# in the environment is not asking -- it is picked up when one of these
|
|
466
|
+
# is given, never used to choose the mode.
|
|
467
|
+
CLOUD_OPTIONS = %i[cloud prompt_token token_stdin url kamal_secrets].freeze
|
|
468
|
+
|
|
469
|
+
def local?
|
|
470
|
+
CLOUD_OPTIONS.none? { |name| options[name] }
|
|
471
|
+
end
|
|
472
|
+
|
|
458
473
|
def resolved_token
|
|
459
474
|
@resolved_token ||= begin
|
|
460
475
|
value = if options[:prompt_token]
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Railwatch: first-class monitoring for Rails. Every option here can also be
|
|
4
4
|
# set by the RAILWATCH_* env var named in the comment.
|
|
5
5
|
Railwatch.configure do |c|
|
|
6
|
-
<% if
|
|
6
|
+
<% if local? -%>
|
|
7
7
|
# Telemetry stays in this app's own railwatch_telemetry database and the
|
|
8
8
|
# dashboard is served at /railwatch. No token, no cloud. Put the mount
|
|
9
9
|
# behind your own authentication; this only names who is looking.
|
|
@@ -13,9 +13,9 @@ Railwatch.configure do |c|
|
|
|
13
13
|
# c.repository_url = "https://github.com/you/app" # RAILWATCH_REPOSITORY_URL; source links from stack traces
|
|
14
14
|
# c.retention_days = 7 # RAILWATCH_RETENTION_DAYS; PruneTelemetryJob keeps this much
|
|
15
15
|
# Access. The dashboard shows every query, log line and exception this app
|
|
16
|
-
# records, so pick one of these. Out of the box it is HTTP Basic
|
|
17
|
-
# closed until credentials exist:
|
|
18
|
-
# bin/rails railwatch:authentication:configure
|
|
16
|
+
# records, so pick one of these. Out of the box it is HTTP Basic: open in
|
|
17
|
+
# development, closed everywhere else until credentials exist:
|
|
18
|
+
# RAILS_ENV=production bin/rails railwatch:authentication:configure
|
|
19
19
|
# or RAILWATCH_HTTP_BASIC_AUTH_USER / _PASSWORD.
|
|
20
20
|
#
|
|
21
21
|
# Using your own admin auth instead? Turn Basic off and say which, so the
|
|
@@ -21,6 +21,11 @@ Puma::Plugin.create do
|
|
|
21
21
|
attr_reader :log_writer, :writer_pid
|
|
22
22
|
|
|
23
23
|
POLL = 2
|
|
24
|
+
# How often a stopping writer is checked for, and how long a KILL is given
|
|
25
|
+
# to take before the pid is abandoned (a process stuck in disk I/O cannot
|
|
26
|
+
# die until the I/O returns, and Puma's exit should not wait for that).
|
|
27
|
+
REAP_POLL = 0.05
|
|
28
|
+
KILL_REAP = 1
|
|
24
29
|
|
|
25
30
|
def start(launcher)
|
|
26
31
|
@log_writer = launcher.log_writer
|
|
@@ -151,19 +156,59 @@ Puma::Plugin.create do
|
|
|
151
156
|
log "Railwatch writer shutdown failed (#{e.class}: #{e.message})"
|
|
152
157
|
end
|
|
153
158
|
|
|
154
|
-
# TERM closes the writer's listener
|
|
155
|
-
#
|
|
159
|
+
# TERM closes the writer's listener; it finishes what it is holding and
|
|
160
|
+
# exits on its own. Waited for with a deadline, not Process.wait, which
|
|
161
|
+
# has none: a writer wedged in a SQLite write or on a full disk would hold
|
|
162
|
+
# Puma's exit open for as long as it stayed wedged. KILL past the deadline.
|
|
163
|
+
# Reaped either way, so a cluster master never leaves a zombie behind.
|
|
156
164
|
def stop_writer
|
|
157
165
|
return unless @writer_pid
|
|
158
166
|
|
|
159
167
|
Process.kill(:TERM, @writer_pid)
|
|
160
|
-
|
|
168
|
+
return if reaped_within?(stop_timeout)
|
|
169
|
+
|
|
170
|
+
log "Railwatch writer (pid #{@writer_pid}) did not exit within #{stop_timeout}s of TERM; killing it"
|
|
171
|
+
Process.kill(:KILL, @writer_pid)
|
|
172
|
+
log "Railwatch writer (pid #{@writer_pid}) did not exit on KILL; leaving it" unless reaped_within?(KILL_REAP)
|
|
161
173
|
rescue Errno::ECHILD, Errno::ESRCH
|
|
162
174
|
nil
|
|
163
175
|
ensure
|
|
164
176
|
@writer_pid = nil
|
|
165
177
|
end
|
|
166
178
|
|
|
179
|
+
# shutdown_timeout: the same allowance this process gives its own
|
|
180
|
+
# reporter, and deliberately NOT the writer's full theoretical exit time
|
|
181
|
+
# (a sequential SHUTDOWN_DRAIN join per worker thread, its maintenance
|
|
182
|
+
# join, then its own reporter shutdown -- 13s at the defaults). Waiting
|
|
183
|
+
# that long would buy nothing: a writer killed mid-batch loses no data,
|
|
184
|
+
# because the transaction rolls back and the worker retries the batch by
|
|
185
|
+
# id against the next writer (Writer#serve says so). Exit time spent
|
|
186
|
+
# waiting for the drain is spent for nothing. An idle writer is gone in
|
|
187
|
+
# well under a second either way.
|
|
188
|
+
#
|
|
189
|
+
# This runs from at_exit, inside the container's TERM-to-KILL grace. Under
|
|
190
|
+
# Kamal that is Docker's 10s default for a proxied role that has not set
|
|
191
|
+
# `stop_timeout`, or whatever `stop_timeout` says when it has; an app that
|
|
192
|
+
# wants the writer given longer can raise RAILWATCH_SHUTDOWN_TIMEOUT to
|
|
193
|
+
# match its grace, and this bound rises with it.
|
|
194
|
+
def stop_timeout
|
|
195
|
+
[ ::Railwatch.config.shutdown_timeout.to_f, 0.0 ].max
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Non-blocking waits on a short poll; Process.wait has no timeout and a
|
|
199
|
+
# child that never exits would hold it forever. ECHILD (the cluster's
|
|
200
|
+
# wait2(-1) reaped it first) propagates to stop_writer, which reads it as
|
|
201
|
+
# gone.
|
|
202
|
+
def reaped_within?(seconds)
|
|
203
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds
|
|
204
|
+
loop do
|
|
205
|
+
return true if Process.waitpid(@writer_pid, Process::WNOHANG)
|
|
206
|
+
return false if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
207
|
+
|
|
208
|
+
sleep REAP_POLL
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
167
212
|
def log(message)
|
|
168
213
|
log_writer.log(message)
|
|
169
214
|
end
|
|
@@ -81,7 +81,7 @@ module Railwatch
|
|
|
81
81
|
:max_view_renders_per_execution, :ignored_cache_key_prefixes,
|
|
82
82
|
:beacon_enabled, :beacon_rate_limit, :beacon_global_rate_limit, :beacon_allowed_origins,
|
|
83
83
|
:debug, :capture_default_vendor_commands,
|
|
84
|
-
:capture_default_vendor_cache_keys, :on_unrecoverable,
|
|
84
|
+
:capture_default_vendor_cache_keys, :on_unrecoverable, :warn_on_data_loss,
|
|
85
85
|
:capture_framework_events,
|
|
86
86
|
:tail_sample_slow_ms, :failure_context, :propagate_traces, :trace_propagation_hosts,
|
|
87
87
|
:health_interval, :capture_query_explain, :explain_threshold_ms,
|
|
@@ -195,6 +195,12 @@ module Railwatch
|
|
|
195
195
|
@capture_default_vendor_cache_keys = env_bool("RAILWATCH_CAPTURE_DEFAULT_VENDOR_CACHE_KEYS", false)
|
|
196
196
|
@capture_framework_events = env_bool("RAILWATCH_CAPTURE_FRAMEWORK_EVENTS", false)
|
|
197
197
|
@on_unrecoverable = nil
|
|
198
|
+
# Off, because a gem printing into an application's own output is the
|
|
199
|
+
# gem changing that application's behaviour, and this one stays
|
|
200
|
+
# additive. Turn it on and a batch lost for good says so in one stderr
|
|
201
|
+
# line; leave it off and the loss shows behind RAILWATCH_DEBUG, or
|
|
202
|
+
# wherever on_unrecoverable routes it.
|
|
203
|
+
@warn_on_data_loss = env_bool("RAILWATCH_WARN_ON_DATA_LOSS", true)
|
|
198
204
|
@beacon_enabled = env_bool("RAILWATCH_BEACON", true)
|
|
199
205
|
# The beacon is unauthenticated and forces Railwatch.keep! for browser
|
|
200
206
|
# errors, so without a ceiling anyone can spend an app's event quota
|
|
@@ -372,12 +378,21 @@ module Railwatch
|
|
|
372
378
|
http_basic_auth_user.to_s.strip != "" && http_basic_auth_password.to_s.strip != ""
|
|
373
379
|
end
|
|
374
380
|
|
|
381
|
+
# Development with Basic on and nothing configured is open, so a first
|
|
382
|
+
# run is `rails g railwatch:install` and a page, not a password step
|
|
383
|
+
# first. Rails already shows full error pages there. Every other
|
|
384
|
+
# environment stays closed until credentials exist.
|
|
385
|
+
def http_basic_auth_waived?
|
|
386
|
+
http_basic_auth_enabled && !http_basic_auth_configured? && defined?(Rails) && Rails.env.development?
|
|
387
|
+
end
|
|
388
|
+
|
|
375
389
|
# Whether the request carries the configured HTTP Basic credentials.
|
|
376
390
|
# False when Basic is on and nothing is configured (closed), true when
|
|
377
391
|
# Basic is off (the host's base controller or routes constraint is the
|
|
378
392
|
# gate then). Shared by the dashboard controller and the live channel.
|
|
379
393
|
def http_basic_auth_ok?(request)
|
|
380
394
|
return true unless http_basic_auth_enabled
|
|
395
|
+
return true if http_basic_auth_waived?
|
|
381
396
|
return false unless http_basic_auth_configured?
|
|
382
397
|
|
|
383
398
|
ActionController::HttpAuthentication::Basic.authenticate(request) do |user, password|
|
data/lib/railwatch/engine.rb
CHANGED
|
@@ -91,16 +91,27 @@ module Railwatch
|
|
|
91
91
|
config.http_basic_auth_password ||= app.credentials.dig(:railwatch, :http_basic_auth_password)
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
-
#
|
|
94
|
+
# Three things worth one line in the log at boot, because all are
|
|
95
95
|
# invisible until something is already wrong: a Rails/json pair that
|
|
96
|
-
# cannot decode,
|
|
97
|
-
#
|
|
96
|
+
# cannot decode, an embedded dashboard with nothing declared in front of
|
|
97
|
+
# it, and one that is closed to everyone because Basic has no
|
|
98
|
+
# credentials outside development.
|
|
98
99
|
initializer "railwatch.warnings", after: :load_config_initializers do
|
|
99
100
|
config.after_initialize do
|
|
100
101
|
next unless Railwatch.enabled?
|
|
101
102
|
|
|
102
103
|
Rails.logger.warn("[railwatch] #{Railwatch::JsonCompat.advice}") if Railwatch::JsonCompat.broken?
|
|
103
104
|
|
|
105
|
+
if Railwatch.config.local? && Railwatch.config.dashboard_gate == :basic &&
|
|
106
|
+
!Railwatch.config.http_basic_auth_configured? && !Rails.env.local?
|
|
107
|
+
Rails.logger.warn(
|
|
108
|
+
"[railwatch] the dashboard is closed: HTTP Basic is on and no credentials are configured for " \
|
|
109
|
+
"#{Rails.env}, so every request to it is 401. Run `RAILS_ENV=#{Rails.env} bin/rails " \
|
|
110
|
+
"railwatch:authentication:configure`, or gate it with your own auth and set " \
|
|
111
|
+
"`c.http_basic_auth_enabled = false` (docs/embedded.md)."
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
104
115
|
if Railwatch.config.local? && Railwatch.config.dashboard_gate == :undeclared && !Rails.env.local?
|
|
105
116
|
Rails.logger.warn(
|
|
106
117
|
"[railwatch] the dashboard at the engine's mount has no gate this gem can see: HTTP Basic is off and " \
|
data/lib/railwatch/reporter.rb
CHANGED
|
@@ -100,10 +100,17 @@ module Railwatch
|
|
|
100
100
|
|
|
101
101
|
def flush
|
|
102
102
|
ensure_process!
|
|
103
|
-
|
|
103
|
+
deferred = nil
|
|
104
|
+
result = @flush_mutex.synchronize do
|
|
105
|
+
@deferred_notifications = []
|
|
104
106
|
update_backpressure
|
|
105
107
|
deliver_buffer
|
|
108
|
+
ensure
|
|
109
|
+
deferred = @deferred_notifications
|
|
110
|
+
@deferred_notifications = nil
|
|
106
111
|
end
|
|
112
|
+
deferred&.each { |error| Railwatch.notify_unrecoverable(error) }
|
|
113
|
+
result
|
|
107
114
|
end
|
|
108
115
|
|
|
109
116
|
def ensure_thread
|
|
@@ -278,6 +285,19 @@ module Railwatch
|
|
|
278
285
|
end
|
|
279
286
|
end
|
|
280
287
|
|
|
288
|
+
# Everything reachable from deliver_buffer runs inside @flush_mutex, so
|
|
289
|
+
# the loss it reports cannot be handed to the application there. Ruby's
|
|
290
|
+
# Mutex is not reentrant: the documented callback is Rails.error.report,
|
|
291
|
+
# whose subscriber records the error as an exception and can end up asking
|
|
292
|
+
# this same reporter to flush -- "ThreadError: deadlock; recursive
|
|
293
|
+
# locking", rescued by notify_unrecoverable and so a callback cut off
|
|
294
|
+
# halfway, reporting nothing. Collected here and dispatched by flush once
|
|
295
|
+
# the lock is released. retain already does this for @mutex; @flush_mutex
|
|
296
|
+
# is the outer one it still sat inside.
|
|
297
|
+
def defer_notification(error)
|
|
298
|
+
@deferred_notifications ? @deferred_notifications << error : Railwatch.notify_unrecoverable(error)
|
|
299
|
+
end
|
|
300
|
+
|
|
281
301
|
def deliver_buffer
|
|
282
302
|
# A 401 was reported once, when the transport first saw it; after
|
|
283
303
|
# that the token is wrong until the process restarts, and repeating
|
|
@@ -337,7 +357,7 @@ module Railwatch
|
|
|
337
357
|
rescue StandardError => e
|
|
338
358
|
result = Transport::Http::Result.new(ok: false, error: "#{e.class}: #{e.message}")
|
|
339
359
|
batch&.records&.any? ? retain(batch, result) : delivery_succeeded
|
|
340
|
-
|
|
360
|
+
defer_notification(e)
|
|
341
361
|
result
|
|
342
362
|
ensure
|
|
343
363
|
in_flight(0, 0, 0, 0)
|
|
@@ -350,7 +370,7 @@ module Railwatch
|
|
|
350
370
|
end
|
|
351
371
|
|
|
352
372
|
def retain(batch, result)
|
|
353
|
-
@mutex.synchronize do
|
|
373
|
+
gave_up = @mutex.synchronize do
|
|
354
374
|
@in_flight_records = 0
|
|
355
375
|
@in_flight_dropped = 0
|
|
356
376
|
@in_flight_bytes = 0
|
|
@@ -362,17 +382,7 @@ module Railwatch
|
|
|
362
382
|
@retry_attempt = 0
|
|
363
383
|
@retry_at = nil
|
|
364
384
|
Railwatch.debug { "gave up on a batch of #{batch.records.size} records after #{MAX_RETRY_ATTEMPTS} retries (#{result.error || result.status}); dropped and counted" }
|
|
365
|
-
|
|
366
|
-
# embedded writer) that never comes back this is the only place the
|
|
367
|
-
# loss is ever reported, and the dropped counter it leaves behind
|
|
368
|
-
# rides on the NEXT successful delivery, which may never happen.
|
|
369
|
-
Railwatch.notify_unrecoverable(
|
|
370
|
-
DeliveryError.new("Railwatch dropped #{batch.records.size} records after #{MAX_RETRY_ATTEMPTS} failed delivery attempts: " \
|
|
371
|
-
"#{result.error || result.status}",
|
|
372
|
-
status: result.status, records: batch.records.size, bytes: batch.bytes,
|
|
373
|
-
dropped: batch.dropped, dropped_bytes: batch.dropped_bytes)
|
|
374
|
-
)
|
|
375
|
-
next
|
|
385
|
+
next true
|
|
376
386
|
end
|
|
377
387
|
@retry_batch = batch
|
|
378
388
|
delay = retry_delay(@retry_attempt)
|
|
@@ -382,7 +392,32 @@ module Railwatch
|
|
|
382
392
|
"retained #{batch.records.size} records after retryable delivery failure " \
|
|
383
393
|
"(#{result.error || result.status}); retry #{@retry_attempt} in #{delay.round(3)}s"
|
|
384
394
|
end
|
|
395
|
+
false
|
|
385
396
|
end
|
|
397
|
+
return unless gave_up
|
|
398
|
+
|
|
399
|
+
# Losing a batch is not a debug-level event: with an ingest (or an
|
|
400
|
+
# embedded writer) that never comes back this is the only place the
|
|
401
|
+
# loss is ever reported, and the dropped counter it leaves behind
|
|
402
|
+
# rides on the NEXT successful delivery, which may never happen.
|
|
403
|
+
#
|
|
404
|
+
# Reported here, after the lock is released, never inside it. The
|
|
405
|
+
# callback is the app's: the documented one is Rails.error.report,
|
|
406
|
+
# whose subscriber records the error as an exception and so writes
|
|
407
|
+
# straight back into this reporter -- which needs @mutex to arm the
|
|
408
|
+
# thread or ask for a flush, and under the lock that was
|
|
409
|
+
# "ThreadError: deadlock; recursive locking" and a callback cut off
|
|
410
|
+
# halfway. A slow callback under the lock was worse: shutdown's own
|
|
411
|
+
# @mutex.synchronize and every request thread's write_now sat behind
|
|
412
|
+
# it for as long as it took. notify_unsent and delivery_rejected
|
|
413
|
+
# already call out unlocked; this was the one that did not.
|
|
414
|
+
defer_notification(
|
|
415
|
+
DeliveryError.new("Railwatch dropped #{batch.records.size} #{batch.records.size == 1 ? "record" : "records"} " \
|
|
416
|
+
"after #{MAX_RETRY_ATTEMPTS + 1} failed delivery attempts: " \
|
|
417
|
+
"#{result.error || result.status}",
|
|
418
|
+
status: result.status, records: batch.records.size, bytes: batch.bytes,
|
|
419
|
+
dropped: batch.dropped, dropped_bytes: batch.dropped_bytes)
|
|
420
|
+
)
|
|
386
421
|
end
|
|
387
422
|
|
|
388
423
|
def discard_unauthorized
|
|
@@ -406,7 +441,7 @@ module Railwatch
|
|
|
406
441
|
def delivery_rejected(batch, result)
|
|
407
442
|
delivery_succeeded
|
|
408
443
|
detail = result.error.to_s.empty? ? "HTTP #{result.status}" : result.error
|
|
409
|
-
|
|
444
|
+
defer_notification(
|
|
410
445
|
DeliveryError.new("Railwatch ingest permanently rejected #{batch.records.size} records: #{detail}",
|
|
411
446
|
status: result.status, records: batch.records.size, bytes: batch.bytes,
|
|
412
447
|
dropped: batch.dropped, dropped_bytes: batch.dropped_bytes)
|
|
@@ -550,7 +585,8 @@ module Railwatch
|
|
|
550
585
|
return unless should_notify
|
|
551
586
|
|
|
552
587
|
Railwatch.notify_unrecoverable(
|
|
553
|
-
DeliveryError.new("Railwatch #{reason} with #{records} unsent records
|
|
588
|
+
DeliveryError.new("Railwatch #{reason} with #{records} unsent #{records == 1 ? "record" : "records"} " \
|
|
589
|
+
"retained in memory (#{bytes} bytes)",
|
|
554
590
|
records: records, bytes: bytes, dropped: dropped, dropped_bytes: dropped_bytes)
|
|
555
591
|
)
|
|
556
592
|
end
|
|
@@ -8,9 +8,10 @@ require "json"
|
|
|
8
8
|
|
|
9
9
|
module Railwatch
|
|
10
10
|
module Transport
|
|
11
|
-
# POSTs gzip NDJSON batches to the platform. Each call
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# POSTs gzip NDJSON batches to the platform. Each call makes exactly one
|
|
12
|
+
# attempt and returns a classified, non-raising result; Reporter owns
|
|
13
|
+
# retention, the retry ladder, and backoff between calls, so a retry here
|
|
14
|
+
# would multiply into its schedule rather than add to it. A 401 marks the
|
|
14
15
|
# transport unauthorized so no further requests are made.
|
|
15
16
|
class Http
|
|
16
17
|
# The request headers a delivery carries besides the body. The receiver
|
|
@@ -87,17 +88,11 @@ module Railwatch
|
|
|
87
88
|
dropped += over_cap
|
|
88
89
|
dropped_bytes += over_cap_bytes
|
|
89
90
|
end
|
|
90
|
-
attempt = 0
|
|
91
91
|
begin
|
|
92
|
-
attempt += 1
|
|
93
92
|
result = parse(post(body, dropped, dropped_bytes, backpressure_factor, batch_id), expected_count: sent)
|
|
94
|
-
if attempt < 2 && (500..599).cover?(result.status)
|
|
95
|
-
result = parse(post(body, dropped, dropped_bytes, backpressure_factor, batch_id), expected_count: sent)
|
|
96
|
-
end
|
|
97
93
|
apply_status_policy(result)
|
|
98
94
|
result
|
|
99
95
|
rescue StandardError => e
|
|
100
|
-
retry if attempt < 2
|
|
101
96
|
Result.new(ok: false, error: "#{e.class}: #{e.message}")
|
|
102
97
|
end
|
|
103
98
|
end
|
data/lib/railwatch/version.rb
CHANGED
data/lib/railwatch.rb
CHANGED
|
@@ -343,10 +343,32 @@ module Railwatch
|
|
|
343
343
|
|
|
344
344
|
# Called (rescued) whenever the gem itself rescues an internal exception:
|
|
345
345
|
# a subscriber block raising, or delivery failing after its retry.
|
|
346
|
-
#
|
|
346
|
+
#
|
|
347
|
+
# With no callback registered, an internal error goes to the debug log:
|
|
348
|
+
# the gem recovered, and a subscriber that raises once is noise in
|
|
349
|
+
# someone else's cron output. A DeliveryError is different. It is raised
|
|
350
|
+
# only when records are already gone -- a batch dropped after the retry
|
|
351
|
+
# ladder, one the receiver permanently refused, or the ones still unsent
|
|
352
|
+
# when at_exit's bounded shutdown ran out of time -- and since 0.3.7 that
|
|
353
|
+
# shutdown is the only delivery a rake task or runner gets. Behind
|
|
354
|
+
# RAILWATCH_DEBUG that loss was invisible: a cron job whose exception
|
|
355
|
+
# never reached the platform looked exactly like one that had nothing to
|
|
356
|
+
# say.
|
|
357
|
+
#
|
|
358
|
+
# It is still not printed by default. A monitoring gem writing into
|
|
359
|
+
# someone else's cron output is the gem changing their application's
|
|
360
|
+
# behaviour, which is not a trade this one makes to report on itself: it
|
|
361
|
+
# stays additive and out of the way. Ask for it with warn_on_data_loss and
|
|
362
|
+
# it becomes one stderr line, or register on_unrecoverable and route it
|
|
363
|
+
# wherever the app already sends such things. Kernel#warn, not
|
|
364
|
+
# Rails.logger, for the same reason as #debug: it must never become a log
|
|
365
|
+
# record about itself.
|
|
347
366
|
def notify_unrecoverable(error)
|
|
348
367
|
if config.on_unrecoverable
|
|
349
368
|
config.on_unrecoverable.call(error)
|
|
369
|
+
elsif error.is_a?(Reporter::DeliveryError) && config.warn_on_data_loss
|
|
370
|
+
warn("[railwatch] #{error.message}. Register Railwatch.on_unrecoverable to route this elsewhere, " \
|
|
371
|
+
"or set warn_on_data_loss = false to silence it.")
|
|
350
372
|
else
|
|
351
373
|
debug { "unrecoverable internal error: #{error.class}: #{error.message}" }
|
|
352
374
|
end
|
|
@@ -599,6 +621,18 @@ module Railwatch
|
|
|
599
621
|
File.expand_path("../db/#{database}_migrate", __dir__)
|
|
600
622
|
end
|
|
601
623
|
|
|
624
|
+
# Where links the models build (a trace from an execution page, a saved
|
|
625
|
+
# view's page) resolve. The engine's own routes serve the embedded
|
|
626
|
+
# dashboard; a host that mounts these pages on its own routes, as
|
|
627
|
+
# Railwatch Cloud does, points this at them.
|
|
628
|
+
def self.url_helpers
|
|
629
|
+
@url_helpers || Engine.routes.url_helpers
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def self.url_helpers=(helpers)
|
|
633
|
+
@url_helpers = helpers
|
|
634
|
+
end
|
|
635
|
+
|
|
602
636
|
# The one browser client, relative to the gem root. The dashboard bundle
|
|
603
637
|
# is built from it, the install generator copies it, the gem ships it (the
|
|
604
638
|
# only app/frontend file it does), and a host with its own frontend build
|
|
@@ -235,7 +235,7 @@ namespace :railwatch do
|
|
|
235
235
|
%w[railwatch railwatch_telemetry].each do |name|
|
|
236
236
|
configured = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: name)
|
|
237
237
|
check.call(!configured.nil?, "#{name} database",
|
|
238
|
-
configured ? configured.database : "not in config/database.yml (bin/rails generate railwatch:install
|
|
238
|
+
configured ? configured.database : "not in config/database.yml (bin/rails generate railwatch:install)",
|
|
239
239
|
fatal: true)
|
|
240
240
|
end
|
|
241
241
|
{ "railwatch_telemetry" => Railwatch::TelemetryRecord, "railwatch" => Railwatch::ApplicationRecord }.each do |name, base|
|
|
@@ -326,12 +326,19 @@ namespace :railwatch do
|
|
|
326
326
|
# HTTP Basic is on and closed until credentials exist; a deploy that
|
|
327
327
|
# forgot gets a 401, not a public page, and this says so first.
|
|
328
328
|
gate = config.dashboard_gate
|
|
329
|
-
check.call(gate != :undeclared && !(gate == :basic && !config.http_basic_auth_configured?
|
|
329
|
+
check.call(gate != :undeclared && !(gate == :basic && !config.http_basic_auth_configured? && !config.http_basic_auth_waived?),
|
|
330
|
+
"dashboard access",
|
|
330
331
|
case gate
|
|
331
332
|
when :basic
|
|
332
|
-
config.http_basic_auth_configured?
|
|
333
|
+
if config.http_basic_auth_configured?
|
|
334
|
+
"HTTP Basic, user #{config.http_basic_auth_user}"
|
|
335
|
+
elsif config.http_basic_auth_waived?
|
|
336
|
+
"open in development. Production stays closed until you run " \
|
|
337
|
+
"`RAILS_ENV=production bin/rails railwatch:authentication:configure`"
|
|
338
|
+
else
|
|
333
339
|
"closed: HTTP Basic is on with no credentials, so every dashboard request is 401. " \
|
|
334
|
-
|
|
340
|
+
"Run `bin/rails railwatch:authentication:configure`"
|
|
341
|
+
end
|
|
335
342
|
when :controller then "your own: c.base_controller_class = #{config.base_controller_class}"
|
|
336
343
|
when :resolver then "your own: c.dashboard_user decides, and live updates follow it"
|
|
337
344
|
when :open then "deliberately open: anyone who can reach the mount can read it (c.dashboard_open)"
|
data/llms.txt
CHANGED
|
@@ -4,12 +4,16 @@
|
|
|
4
4
|
> requests, jobs, scheduled tasks, rake/runner commands, database queries and
|
|
5
5
|
> N+1s, exceptions, cache, mail, notifications, broadcasts, outgoing HTTP,
|
|
6
6
|
> Active Storage, view renders, and logs — links every one of them into a
|
|
7
|
-
> single execution tree by `execution_id`/`trace_id`, and
|
|
8
|
-
>
|
|
7
|
+
> single execution tree by `execution_id`/`trace_id`, and writes them from a
|
|
8
|
+
> background thread into the app's own SQLite files (embedded, the default) or
|
|
9
|
+
> to Railwatch Cloud. It replaces a separate APM and a
|
|
9
10
|
> separate error tracker with one gem and one configuration block, costs
|
|
10
11
|
> under a millisecond of CPU per request, and never writes to the
|
|
11
12
|
> application's own database. Install is `bundle add railwatch` followed by
|
|
12
|
-
> `bin/rails generate railwatch:install`, which
|
|
13
|
+
> `bin/rails generate railwatch:install`, which by default keeps everything in
|
|
14
|
+
> the app (two SQLite databases, the dashboard at `/railwatch`); with
|
|
15
|
+
> `--cloud` or a token option it ships to Railwatch Cloud instead. It writes
|
|
16
|
+
> the initializer, mounts
|
|
13
17
|
> the beacon engine, adds a Kamal post-deploy hook and the Inertia browser
|
|
14
18
|
> client where the app has them, wires the test matchers, and then runs
|
|
15
19
|
> `bin/rails railwatch:doctor` to print a ✓/✗ line for every piece.
|
|
@@ -31,7 +35,7 @@
|
|
|
31
35
|
|
|
32
36
|
## For coding agents
|
|
33
37
|
|
|
34
|
-
- [AGENTS.md](AGENTS.md): how to install and use Railwatch from inside a Rails app — the facade methods, the spec matchers, `railwatch:doctor`, and the MCP hookup.
|
|
38
|
+
- [AGENTS.md](AGENTS.md): how to install and use Railwatch from inside a Rails app — the facade methods, the spec matchers, `railwatch:doctor`, and the MCP hookup.
|
|
35
39
|
|
|
36
40
|
## Optional
|
|
37
41
|
|