railwatch 0.1.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 +7 -0
- data/AGENTS.md +122 -0
- data/CHANGELOG.md +462 -0
- data/MIT-LICENSE +20 -0
- data/README.md +226 -0
- data/app/controllers/railwatch/beacon_controller.rb +254 -0
- data/config/routes.rb +5 -0
- data/docs/ai-and-mcp.md +227 -0
- data/docs/configuration.md +931 -0
- data/docs/faq.md +230 -0
- data/docs/getting-started.md +279 -0
- data/docs/records.md +834 -0
- data/docs/replacing-nightwatch.md +216 -0
- data/docs/replacing-sentry.md +573 -0
- data/docs/security.md +94 -0
- data/docs/self-hosting.md +60 -0
- data/docs/source-maps.md +60 -0
- data/docs/testing.md +175 -0
- data/docs/troubleshooting.md +319 -0
- data/lib/generators/railwatch/install/install_generator.rb +280 -0
- data/lib/generators/railwatch/install/templates/initializer.rb +54 -0
- data/lib/generators/railwatch/install/templates/post-deploy +98 -0
- data/lib/generators/railwatch/install/templates/railwatch.ts +658 -0
- data/lib/railwatch/attachments.rb +83 -0
- data/lib/railwatch/backtrace.rb +158 -0
- data/lib/railwatch/buffer.rb +122 -0
- data/lib/railwatch/clock.rb +25 -0
- data/lib/railwatch/configuration.rb +334 -0
- data/lib/railwatch/console.rb +48 -0
- data/lib/railwatch/context.rb +125 -0
- data/lib/railwatch/controller_helpers.rb +21 -0
- data/lib/railwatch/current.rb +32 -0
- data/lib/railwatch/engine.rb +144 -0
- data/lib/railwatch/execution.rb +367 -0
- data/lib/railwatch/faraday.rb +73 -0
- data/lib/railwatch/health.rb +188 -0
- data/lib/railwatch/job_tracing.rb +49 -0
- data/lib/railwatch/middleware/request.rb +289 -0
- data/lib/railwatch/minitest.rb +43 -0
- data/lib/railwatch/patches/inertia.rb +34 -0
- data/lib/railwatch/patches/net_http.rb +102 -0
- data/lib/railwatch/patches/rake_task.rb +88 -0
- data/lib/railwatch/patches/runner_command.rb +120 -0
- data/lib/railwatch/patches.rb +43 -0
- data/lib/railwatch/profiler.rb +270 -0
- data/lib/railwatch/record.rb +119 -0
- data/lib/railwatch/redactor.rb +67 -0
- data/lib/railwatch/release_detector.rb +97 -0
- data/lib/railwatch/reporter.rb +539 -0
- data/lib/railwatch/rspec.rb +139 -0
- data/lib/railwatch/sampler.rb +17 -0
- data/lib/railwatch/secret_safety.rb +62 -0
- data/lib/railwatch/sessions.rb +162 -0
- data/lib/railwatch/source_maps.rb +59 -0
- data/lib/railwatch/spec_helper.rb +147 -0
- data/lib/railwatch/sql_normalizer.rb +398 -0
- data/lib/railwatch/subscribers/base.rb +54 -0
- data/lib/railwatch/subscribers/broadcasts.rb +107 -0
- data/lib/railwatch/subscribers/cache.rb +107 -0
- data/lib/railwatch/subscribers/deprecations.rb +26 -0
- data/lib/railwatch/subscribers/exceptions.rb +304 -0
- data/lib/railwatch/subscribers/jobs.rb +282 -0
- data/lib/railwatch/subscribers/logs.rb +137 -0
- data/lib/railwatch/subscribers/mail.rb +42 -0
- data/lib/railwatch/subscribers/notifications.rb +36 -0
- data/lib/railwatch/subscribers/process_info.rb +98 -0
- data/lib/railwatch/subscribers/queries.rb +183 -0
- data/lib/railwatch/subscribers/requests.rb +94 -0
- data/lib/railwatch/subscribers/storage.rb +35 -0
- data/lib/railwatch/subscribers/users.rb +159 -0
- data/lib/railwatch/subscribers/views.rb +54 -0
- data/lib/railwatch/subscribers.rb +34 -0
- data/lib/railwatch/transport/http.rb +208 -0
- data/lib/railwatch/version.rb +5 -0
- data/lib/railwatch.rb +550 -0
- data/lib/tasks/railwatch_tasks.rake +289 -0
- data/llms.txt +38 -0
- metadata +157 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Coming from Laravel Nightwatch
|
|
2
|
+
|
|
3
|
+
Railwatch is the same product shape for Rails: one package instruments the
|
|
4
|
+
framework end to end, records are grouped under the execution that
|
|
5
|
+
produced them, and a hosted platform turns them into routes, jobs,
|
|
6
|
+
queries, issues, and alerts. If you know Nightwatch, you already know
|
|
7
|
+
how to read Railwatch — this page maps the vocabulary and points out the
|
|
8
|
+
three places the Rails answer is genuinely different.
|
|
9
|
+
|
|
10
|
+
Railwatch is an independent product and is not affiliated with Laravel or
|
|
11
|
+
Laravel Nightwatch.
|
|
12
|
+
|
|
13
|
+
## The architecture difference: no agent
|
|
14
|
+
|
|
15
|
+
Nightwatch needs `nightwatch:agent` because PHP-FPM has no long-lived
|
|
16
|
+
process to batch from: the app writes records over a socket and a
|
|
17
|
+
separate daemon POSTs them.
|
|
18
|
+
|
|
19
|
+
Puma and Solid Queue workers *are* long-lived, so Railwatch skips that
|
|
20
|
+
tier. A single reporter thread per process holds a bounded buffer
|
|
21
|
+
(default 5,000 records, oldest dropped and counted), and gzip-NDJSON
|
|
22
|
+
POSTs batches to the platform every 2 seconds or every 500 records. There
|
|
23
|
+
is no daemon to install, supervise, or forget to restart, and nothing
|
|
24
|
+
between the app and the ingest URL.
|
|
25
|
+
|
|
26
|
+
The thread is re-armed after `fork`, so clustered Puma workers and
|
|
27
|
+
forked Solid Queue workers each get their own with no `on_worker_boot`
|
|
28
|
+
hook to write.
|
|
29
|
+
|
|
30
|
+
## Records
|
|
31
|
+
|
|
32
|
+
Nightwatch's types, and what they're called here:
|
|
33
|
+
|
|
34
|
+
| Nightwatch | Railwatch | Notes |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `request` | `request` | Plus `controller`/`action`, `format`, `queue_time` from `X-Request-Start`, `view_runtime`/`db_runtime`, `redirect_to`, `halted_callback`, `unpermitted_parameters`, `rate_limited`, and an `inertia` block (component, version, partial reload, prop bytes, SSR ms). |
|
|
37
|
+
| `command` | `command` | Rake tasks and `bin/rails runner`, not Artisan — Rails has no command bus. Task prerequisites nest inside the top-level command rather than opening their own. |
|
|
38
|
+
| `job-attempt` | `job_attempt` | Active Job level, so the adapter (Solid Queue, Sidekiq, ...) doesn't matter. Adds `attempt`, `queue_latency`, `concurrency_key`, `priority`, and a `"released"` status for a `retry_on` that caught internally. |
|
|
39
|
+
| `scheduled-task` | `scheduled_task` | Solid Queue recurring tasks from `config/recurring.yml`, detected from `SolidQueue::RecurringExecution`. Carries `task_key`, `schedule`, and `drift`. |
|
|
40
|
+
| — | `channel_action` | One Action Cable action parent with its SQL, logs, broadcasts/transmits, and exception in the same trace. |
|
|
41
|
+
| `query` | `query` | Normalized per adapter, with source `file:line`. Cached queries are counted, not stored. |
|
|
42
|
+
| — | `n_plus_one` | Derived in-process: the same query group repeating `n_plus_one_threshold` times (default 5) in one execution. |
|
|
43
|
+
| — | `transaction` | Duration, outcome, statement count. |
|
|
44
|
+
| `exception` | `exception` | Grouped by class + top app frame + normalized message, overridable per class, per call, or globally. |
|
|
45
|
+
| `cache-event` | `cache_event` | Same hit/miss/write/delete shape, over Active Support::Cache. |
|
|
46
|
+
| `mail` | `mail` | Render ms vs deliver ms, recipient counts, delivery method. |
|
|
47
|
+
| `notification` | `notification` | The `noticed` gem, if loaded. Laravel's channel system has no Rails equivalent. |
|
|
48
|
+
| — | `broadcast` | Action Cable `broadcast`/`transmit` — the closest thing Rails has to Laravel's push channels. |
|
|
49
|
+
| `outgoing-request` | `outgoing_request` | One `Net::HTTP` prepend covers Faraday's default adapter, HTTParty, RestClient, and `ruby-llm`; `Railwatch::Faraday` is the Guzzle-middleware equivalent for other adapters. |
|
|
50
|
+
| `queued-job` | `enqueued_job` | The enqueue side, in the execution that enqueued it. |
|
|
51
|
+
| `log` | `log` | Lines at or above `log_level`, plus Rails 8.1 structured `Rails.event` events. |
|
|
52
|
+
| `user` | `user` | Resolved once per user per process-hour, not once per request. |
|
|
53
|
+
| deployment (`nightwatch:deploy`) | `Deploy` on the platform | Posted by `railwatch:deploy` or the Kamal `post-deploy` hook, with up to 50 commits so the platform can diff what shipped. |
|
|
54
|
+
| request `stages` | parent `stages` | Same idea, Rails boundaries: `middleware_before`, `action`, `render`, `middleware_after`, `body`. Laravel's `bootstrap` has no equivalent in a warm process — Railwatch reports boot time once per process as a `process` record instead. |
|
|
55
|
+
|
|
56
|
+
And the types with no Nightwatch counterpart at all: `storage_op` (Active
|
|
57
|
+
Storage), `view_render`, `span` (your own timed blocks), `attachment`,
|
|
58
|
+
`deprecation`, `visit` (Inertia page visits from the browser),
|
|
59
|
+
`session` (release health), `process`, `health` (Puma pool, Active Record
|
|
60
|
+
pool, Solid Queue backlog), and `profile` (sampled stack profiles).
|
|
61
|
+
|
|
62
|
+
Field-by-field detail for all 26 is in [`records.md`](records.md).
|
|
63
|
+
|
|
64
|
+
## What "execution" means
|
|
65
|
+
|
|
66
|
+
Exactly what it means in Nightwatch, with one more parent type. An
|
|
67
|
+
execution is a **request**, a **job attempt**, a **scheduled task run**,
|
|
68
|
+
or a **command**. Every other record is a child of one: it carries
|
|
69
|
+
`execution_id`, `execution_source`, `execution_preview` (the human
|
|
70
|
+
label, e.g. `"GET /posts"`), and `execution_stage` (which lifecycle stage
|
|
71
|
+
it happened in).
|
|
72
|
+
|
|
73
|
+
`trace_id` is the wider unit. It is shared by a request and every job
|
|
74
|
+
that request enqueued, so async work traces back to what started it, and
|
|
75
|
+
it is propagated across services as a W3C `traceparent` on outgoing HTTP
|
|
76
|
+
— an inbound `traceparent` is adopted, so a trace spans services rather
|
|
77
|
+
than stopping at the process boundary.
|
|
78
|
+
|
|
79
|
+
Practically: on the platform you never look at a query in isolation. You
|
|
80
|
+
open the request, and the query is in its waterfall with everything else
|
|
81
|
+
that execution did.
|
|
82
|
+
|
|
83
|
+
## Sampling parity
|
|
84
|
+
|
|
85
|
+
Nightwatch samples per kind (`sampling.requests`, `.commands`,
|
|
86
|
+
`.exceptions`, `.scheduled_tasks`), decided once per execution. Railwatch
|
|
87
|
+
is the same hash with `jobs` added:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
c.sample = { requests: 0.1, jobs: 1.0, commands: 1.0,
|
|
91
|
+
scheduled_tasks: 1.0, channels: 1.0, exceptions: 1.0 }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Same semantics: sampled in means the whole tree ships, sampled out means
|
|
95
|
+
nothing ships — except an unhandled exception, which is governed by the
|
|
96
|
+
`exceptions` rate and flushed immediately. Counters on the parent
|
|
97
|
+
(queries, cache events, mail, ...) are incremented even when the
|
|
98
|
+
execution is sampled out, so aggregate rates don't depend on the sample
|
|
99
|
+
rate.
|
|
100
|
+
|
|
101
|
+
Nightwatch's `Sample::rate(0.5) / always() / never()` route middleware
|
|
102
|
+
becomes a controller macro:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
class ReportsController < ApplicationController
|
|
106
|
+
railwatch_sample 0.01, only: :index
|
|
107
|
+
railwatch_never_sample only: :health
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Both take the same options as `before_action`. Programmatically:
|
|
112
|
+
`Railwatch.sample(rate)`, `Railwatch.dont_sample`, `Railwatch.sampling?`.
|
|
113
|
+
|
|
114
|
+
**What Railwatch adds: tail sampling.** Head sampling throws away exactly
|
|
115
|
+
the slow requests you wanted. Set `c.tail_sample_slow_ms = 500` and a
|
|
116
|
+
head-sampled-out execution keeps buffering its children and is kept at
|
|
117
|
+
the end if it ran that long, raised, or called `Railwatch.keep!`. Its
|
|
118
|
+
parent record carries `tail_sampled: true` so it stays distinguishable
|
|
119
|
+
from a head-sampled one.
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
c.sample = { requests: 0.05 } # 5% of requests...
|
|
123
|
+
c.tail_sample_slow_ms = 500 # ...plus every one over 500ms
|
|
124
|
+
Railwatch.keep! # ...plus this one, whatever the roll said
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The trade-off is memory: with tail sampling on, every sampled-out
|
|
128
|
+
execution buffers its child records for its lifetime (capped at 10,000
|
|
129
|
+
per execution) instead of discarding them as they happen. With it off —
|
|
130
|
+
the default — nothing is built or buffered for a sampled-out execution
|
|
131
|
+
at all.
|
|
132
|
+
|
|
133
|
+
**And failure context.** Nightwatch, like Railwatch before this, ships an
|
|
134
|
+
unsampled execution's unhandled exception with its parent record and
|
|
135
|
+
nothing else: no queries, logs or outgoing requests from the moments
|
|
136
|
+
before it. Set `c.failure_context = 200` and a head-sampled-out
|
|
137
|
+
execution keeps its last 200 child records in a ring, shipping them only
|
|
138
|
+
if it reports an unhandled exception. It is the failure half of tail
|
|
139
|
+
sampling without the memory bill of keeping every sampled-out execution
|
|
140
|
+
alive; the two are independent, and tail sampling's larger buffer wins if
|
|
141
|
+
both are set. See `docs/configuration.md`.
|
|
142
|
+
|
|
143
|
+
## Facade parity
|
|
144
|
+
|
|
145
|
+
Same facade, Ruby names:
|
|
146
|
+
|
|
147
|
+
| Nightwatch | Railwatch |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `Nightwatch::user($cb)` | `Railwatch.user { \|user\| ... }` (or `c.user { }`) |
|
|
150
|
+
| `sample($rate)` | `Railwatch.sample(rate)` |
|
|
151
|
+
| `dontSample()` | `Railwatch.dont_sample` |
|
|
152
|
+
| `sampling()` | `Railwatch.sampling?` |
|
|
153
|
+
| `ignore($cb)` | `Railwatch.ignore { }` |
|
|
154
|
+
| `pause()` / `resume()` / `paused()` | `Railwatch.pause` / `Railwatch.resume` / `Railwatch.paused?` |
|
|
155
|
+
| `report($e, $handled)` | `Railwatch.report(error, handled: true)` |
|
|
156
|
+
| `redactRequests` / `redactQueries` / `redactExceptions` / `redactCacheEvents` / `redactCommands` / `redactMail` / `redactOutgoingRequests` | `Railwatch.redact_requests`, `redact_queries`, `redact_exceptions`, `redact_cache_events`, `redact_commands`, `redact_mail`, `redact_outgoing_requests`, plus `redact_logs` |
|
|
157
|
+
| `rejectQueries` / `rejectCacheEvents` / `rejectMail` / `rejectNotifications` / `rejectOutgoingRequests` / `rejectQueuedJobs` | `Railwatch.reject_queries`, `reject_cache_events`, `reject_mail`, `reject_notifications`, `reject_outgoing_requests`, `reject_enqueued_jobs`, plus `reject_broadcasts` and `reject_logs` |
|
|
158
|
+
| `rejectCacheKeys([...])` | `Railwatch.reject_cache_keys(%w[session: rack::attack*])` |
|
|
159
|
+
| `captureDefaultVendorCommands` / `CacheKeys` | `c.capture_default_vendor_commands` / `c.capture_default_vendor_cache_keys` |
|
|
160
|
+
| `guzzleMiddleware()` | `Railwatch::Faraday` (`Faraday.new(url) { \|f\| f.use Railwatch::Faraday }`); `Net::HTTP` is covered globally with no setup |
|
|
161
|
+
| `IngestingEvents` listener returning `false` | `Railwatch.before_ingest { \|batch\| ... }` |
|
|
162
|
+
|
|
163
|
+
Beyond the facade: `Railwatch.context(**attrs)` (Laravel Context's
|
|
164
|
+
counterpart, writing through to all three of Rails' own context stores),
|
|
165
|
+
`Railwatch.span(name, **attrs) { }`, `Railwatch.keep!`,
|
|
166
|
+
`Railwatch.attach(name, data)`, `Railwatch.fingerprint { }`,
|
|
167
|
+
`Railwatch.instrument_outgoing(method, url) { }`,
|
|
168
|
+
`Railwatch.on_unrecoverable { }`, and `Railwatch.flush`.
|
|
169
|
+
|
|
170
|
+
## Config and commands
|
|
171
|
+
|
|
172
|
+
`config/nightwatch.php` becomes `config/initializers/railwatch.rb`, and
|
|
173
|
+
every setting still has an env var — `NIGHTWATCH_*` becomes `RAILWATCH_*`.
|
|
174
|
+
`filtering.ignore_*` becomes one list, `c.ignore = [:cache_events,
|
|
175
|
+
:queries, ...]`, validated at assignment. `filtering.log_level` becomes
|
|
176
|
+
`c.log_level`. `ingest.uri`, `.timeout`, `.connection_timeout`, and
|
|
177
|
+
`.event_buffer` become `c.ingest_url`, `c.timeout`,
|
|
178
|
+
`c.connect_timeout`, and `c.buffer_size`.
|
|
179
|
+
|
|
180
|
+
| Nightwatch | Railwatch |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `nightwatch:agent` | Nothing — the reporter thread lives in the app process. |
|
|
183
|
+
| `nightwatch:status` | `bin/rails railwatch:status` |
|
|
184
|
+
| `nightwatch:deploy {deploy} --ref --name --url` | `bin/rails railwatch:deploy[ref,name,url]`, or the generated `.kamal/hooks/post-deploy` |
|
|
185
|
+
| — | `bin/rails railwatch:doctor`, which checks the whole install and exits non-zero if the token or the ingest host is wrong |
|
|
186
|
+
|
|
187
|
+
## The three things worth knowing about Rails
|
|
188
|
+
|
|
189
|
+
**Tenancy is first class.** Nightwatch tells you to prefix user ids by
|
|
190
|
+
hand. Railwatch reads `ActiveRecord::Base.current_tenant` /
|
|
191
|
+
`TenantRecord.current_tenant` (`activerecord-tenanted`) with zero config,
|
|
192
|
+
stamps `tenant` on every record, and gives you a Tenants page.
|
|
193
|
+
`Railwatch.context(tenant: org.slug)` sets it explicitly for apps that
|
|
194
|
+
roll their own.
|
|
195
|
+
|
|
196
|
+
**Jobs are instrumented at Active Job**, not per adapter, so Solid Queue,
|
|
197
|
+
Sidekiq, and anything else with an Active Job adapter all report the same
|
|
198
|
+
`job_attempt` fields. Scheduled tasks are Solid Queue recurring tasks,
|
|
199
|
+
so cron monitoring needs no check-in calls.
|
|
200
|
+
|
|
201
|
+
**Your test suite is a performance gate.** The same instrumentation runs
|
|
202
|
+
under RSpec and Minitest, so a query budget can be checked in and CI can
|
|
203
|
+
fail the pull request that regresses it:
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
expect { get "/widgets" }.to have_railwatch_queries(at_most: 6)
|
|
207
|
+
expect { get "/widgets" }.not_to have_railwatch_n_plus_one
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
See [`testing.md`](testing.md).
|
|
211
|
+
|
|
212
|
+
## Next
|
|
213
|
+
|
|
214
|
+
- [`getting-started.md`](getting-started.md) — install, in five minutes.
|
|
215
|
+
- [`configuration.md`](configuration.md) — every option and env var.
|
|
216
|
+
- [`records.md`](records.md) — all 26 record types, field by field.
|