txray 0.1.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f735a624f2fabb3ab9d3b2133c2b7793d6c836e730a6d71a3a5d21217f49e3d9
4
- data.tar.gz: c76ae3d2e4aa426361fc0b2a834c76d5e33e910af92e541eaecacbe80ea462e9
3
+ metadata.gz: '04384fde1e534819ca3055d82a586c3da880b2ea97aa49d8fbe2c3eaa0182beb'
4
+ data.tar.gz: d8b1dd217628ad5a12780cd295d27bdb7c934b10907443d6afe95726925c62f2
5
5
  SHA512:
6
- metadata.gz: a10c45fade77e209f9c5bdc85bcb4453629ad9a2acf63d0067ff16a5a958512a6e4c31f9e1e54aedb60660e9c021d2513fe17b990aeb254ca607347138b73135
7
- data.tar.gz: 27f2cdcd1bfaceb62be8ebc3ffedca7f90cf53104fc1f25a79af81ef48051d7e8e2d71649d354a184207ca4c7f347c4316fdd6a6795a1ea57442e5b68a35871d
6
+ metadata.gz: 4f2ed21c432a811623ddfae148e50cbdd54283b71fc663f5ca67750fdbe5710c5810ec6ee2c64bc9ce134de420b0195f7f30e8ab0c5c1c54cdcfc2522567791f
7
+ data.tar.gz: 4eab65e95a199496556e0569667b09e414205e0bd8f21fd25af58636ff21a3107ca33b413789a00a8f55d6e4470181b03811e711a02bb007a65d561dfa05b6cc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1
4
+
5
+ Nothing in the analyzer, the runtime guard or the reporters changed. This release exists to exercise the publishing pipeline on something low stakes before a release that matters.
6
+
7
+ - Releases are now published by GitHub Actions through RubyGems trusted publishing, so no API key is created, pasted or stored. Merging a version bump tags the commit, runs the suite, publishes the gem and creates the GitHub release with the notes taken from this file.
8
+ - Corrected the release steps in `CONTRIBUTING.md`, which told contributors to push a version bump straight to a protected `main`.
9
+
10
+ ## 0.2.0
11
+
12
+ - Track clients held in local variables, instance variables, constants, memoized readers and `delegate` targets, so the request is reported rather than the harmless construction.
13
+ - Resolve `send` and `public_send` with a symbol literal, `define_method` bodies and concern `included` blocks; report genuinely dynamic dispatch as a visible low severity blind spot.
14
+ - Cover custom validations, `before_commit`, advisory locks and migration bodies as transactional scope, and add rules for broadcasts and blocking file, CSV and image work.
15
+ - RuboCop style directive regions: `# txray:disable RULE` ... `# txray:enable RULE`, `all`, and whole file suppression.
16
+ - Rebuilt live monitor with stacked meters, a latency histogram and severity colour, clamped to the terminal; `rails generate txray:install`; `NO_COLOR` support.
17
+ - Report paths relative to the working directory, and escape GitHub workflow commands correctly, so annotations and SARIF attach to the right file.
18
+ - `--format github` now writes a job summary table in addition to inline annotations.
19
+ - Runtime guard supports Rails 7.0 through 8.x, timing the transaction directly on versions that do not emit `start_transaction.active_record`.
20
+ - Clear errors for a malformed config, a missing path and an unparseable file, all of which previously reported a clean scan that never happened.
21
+
3
22
  ## 0.1.0
4
23
 
5
24
  - Static transaction analysis over explicit transactions, row and advisory locks, save callbacks, custom validations and migrations.
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,43 @@
1
+ # Contributing
2
+
3
+ ```sh
4
+ bin/setup
5
+ bundle exec rake # specs and rubocop
6
+ ```
7
+
8
+ ## Adding a rule
9
+
10
+ A rule is three things and a test.
11
+
12
+ 1. `Rules.register` in `lib/txray/rule.rb`, with the severity, what the reader sees, and the fix.
13
+ 2. A branch in `lib/txray/classifier.rb`, or a namespace in `lib/txray/catalog.rb` if the call is recognised by its receiver.
14
+ 3. A scenario in `spec/txray/scenarios_spec.rb` that proves it fires, and one that proves it stays quiet on the shape it should ignore.
15
+
16
+ The second test matters more than the first. A rule that fires on correct code costs more than a rule that misses.
17
+
18
+ ## Adding a transactional scope
19
+
20
+ `lib/txray/transaction_scope.rb` decides what counts as being inside a transaction. Anything added there needs a case showing that the corresponding commit-time callback is still left alone.
21
+
22
+ ## Testing against other Rails versions
23
+
24
+ ```sh
25
+ RAILS_VERSION=7.1 bundle install && RAILS_VERSION=7.1 bundle exec rspec
26
+ ```
27
+
28
+ CI runs 7.1, 7.2 and 8.0 alongside Ruby 3.2, 3.3 and 3.4.
29
+
30
+ ## Before opening a pull request
31
+
32
+ Run the scanner against a real application, not only the suite. False positives are the failure mode that matters, and they show up in real code long before they show up in fixtures.
33
+
34
+ ## Releasing
35
+
36
+ Merging a version bump releases the gem. There is nothing to run by hand.
37
+
38
+ 1. On a branch, bump `Txray::VERSION` in `lib/txray/version.rb` and add a `## x.y.z` section to `CHANGELOG.md`.
39
+ 2. Open the pull request and merge it once CI is green.
40
+
41
+ On every push to `main` the release workflow reads `Txray::VERSION` and stops immediately if that version is already tagged, so ordinary merges do nothing. When the version is new it takes the release notes from the matching changelog section, runs the suite, tags the commit, publishes the gem to RubyGems through trusted publishing, and creates the GitHub release with the notes and the built gem attached.
42
+
43
+ The tag is created locally and only pushed once the gem is published, so a failure leaves nothing behind: fix it and merge again. A missing changelog section fails before the suite even runs. No API key is involved anywhere.
data/README.md CHANGED
@@ -16,17 +16,31 @@ app/models/order.rb
16
16
 
17
17
  ## Install
18
18
 
19
+ For the scanner alone:
20
+
19
21
  ```ruby
20
22
  group :development, :test do
21
23
  gem "txray", require: false
22
24
  end
23
25
  ```
24
26
 
27
+ For the scanner **and** the runtime guard and live monitor, drop `require: false`, or the railtie never loads and the guard silently never installs:
28
+
29
+ ```ruby
30
+ group :development, :test do
31
+ gem "txray"
32
+ end
33
+ ```
34
+
35
+ Then:
36
+
25
37
  ```sh
26
38
  bundle exec txray
27
39
  ```
28
40
 
29
- It exits non zero when it finds something, so it drops straight into CI.
41
+ It exits non zero when it finds something, so it drops straight into CI. No configuration is required; everything below has a default.
42
+
43
+ In a Rails app, `bin/rails generate txray:install` writes a commented `.txray.yml` and prints the next steps. Outside Rails, `bundle exec txray --init` writes the same file.
30
44
 
31
45
  ## What counts as a transaction
32
46
 
@@ -158,14 +172,35 @@ Exclusions apply to the path below the root being scanned, so `vendor` means the
158
172
 
159
173
  ### Inline suppression
160
174
 
175
+ Directives follow the same shape as RuboCop's. A trailing comment silences that line:
176
+
161
177
  ```ruby
162
178
  def charge = Faraday.post(url) # txray:disable
179
+ def charge = Faraday.post(url) # txray:disable http-in-transaction
180
+ ```
181
+
182
+ A comment on its own line opens a region that runs until the matching `enable`, or to the end of the file if there is none:
163
183
 
184
+ ```ruby
164
185
  # txray:disable http-in-transaction
165
186
  Faraday.post(url)
187
+ Faraday.get(url)
188
+ # txray:enable http-in-transaction
166
189
  ```
167
190
 
168
- A bare `# txray:disable` disables every rule on that line; naming rules disables only those. The comment works on the offending line or the line directly above it.
191
+ Name several rules with commas, and re-enable them independently:
192
+
193
+ ```ruby
194
+ # txray:disable http-in-transaction, job-enqueue-in-transaction
195
+ ...
196
+ # txray:enable job-enqueue-in-transaction
197
+ ```
198
+
199
+ A bare directive, or `all`, covers every rule. At the top of a file that silences the whole file:
200
+
201
+ ```ruby
202
+ # txray:disable all
203
+ ```
169
204
 
170
205
  ## Command line
171
206
 
@@ -189,19 +224,43 @@ Usage: txray [options] [paths]
189
224
  ## CI
190
225
 
191
226
  ```yaml
192
- - name: Scan for slow transactions
193
- run: bundle exec txray --format github
227
+ name: txray
228
+
229
+ on:
230
+ pull_request:
231
+ push:
232
+ branches: [ main ]
233
+
234
+ jobs:
235
+ transactions:
236
+ runs-on: ubuntu-latest
237
+ steps:
238
+ - uses: actions/checkout@v4
239
+ - uses: ruby/setup-ruby@v1
240
+ with:
241
+ bundler-cache: true
242
+ - name: Scan for slow transactions
243
+ run: bundle exec txray --format github
194
244
  ```
195
245
 
196
- `--format github` writes inline annotations on the pull request. `--format sarif` uploads to GitHub code scanning:
246
+ That one step does three things. It annotates the offending lines inline on the pull request, with the call path and the suggested fix in each annotation. It writes a table of every finding to the job summary, so the run page carries the full list even though GitHub caps how many annotations it will render for a single step. And it exits non zero, so the check fails.
247
+
248
+ Use `--fail-level high` to annotate everything but only fail the build on the high severity rules, or `--fail-level none` to report without ever failing while you work through a backlog.
249
+
250
+ To send findings to GitHub code scanning instead, so they appear in the Security tab and track across branches:
197
251
 
198
252
  ```yaml
199
- - run: bundle exec txray --format sarif > txray.sarif || true
200
- - uses: github/codeql-action/upload-sarif@v3
201
- with:
202
- sarif_file: txray.sarif
253
+ - run: bundle exec txray --format sarif > txray.sarif
254
+ continue-on-error: true
255
+ - uses: github/codeql-action/upload-sarif@v3
256
+ with:
257
+ sarif_file: txray.sarif
203
258
  ```
204
259
 
260
+ That job needs `permissions: security-events: write`. `continue-on-error` matters: without it a failing scan skips the upload step, and the findings never arrive.
261
+
262
+ Paths are reported relative to the working directory in every format, which is what GitHub needs to match a finding to a file in the repository.
263
+
205
264
  ## Runtime guard
206
265
 
207
266
  Static analysis cannot see through metaprogramming, gem internals or a client handed in as an argument. The optional runtime guard catches what is left, from inside a running application:
@@ -215,6 +274,10 @@ runtime:
215
274
 
216
275
  It reports a transaction that stays open past the threshold, a job enqueued while a transaction is open, mail delivered while a transaction is open, and any `Net::HTTP` request made while a transaction is open (which covers Faraday, HTTParty, RestClient, Octokit and everything else built on it). Each finding is attributed to the transaction that was open, with the duration of the offending call and the application frame that caused it. `on_violation: log` warns, `raise` fails loudly. Run it as `raise` in test and `log` in development.
217
276
 
277
+ ### Rails versions
278
+
279
+ The analyzer has no Rails dependency at all. The runtime guard supports **Rails 7.0 through 8.x** and picks its hook accordingly. From Rails 7.2 it subscribes to the `start_transaction.active_record` and `transaction.active_record` notifications. Rails 7.0 and 7.1 do not emit `start_transaction`, and the duration carried by `transaction.active_record` is not the duration of the transaction, so on those versions the guard times `within_new_transaction` itself. Both paths report the same real duration, attribute findings to the transaction that was open, and record the commit or rollback outcome; both are covered by the test suite and exercised in CI against 7.1, 7.2 and 8.0.
280
+
218
281
  Known-good work can be excused:
219
282
 
220
283
  ```ruby
@@ -229,30 +292,48 @@ end
229
292
 
230
293
  The guard writes newline delimited JSON to `runtime.log_path`. `txray watch` tails it and renders what your application is doing right now:
231
294
 
232
- ```
233
- txray watching tmp/txray.ndjson 00:04:12
295
+ ![the txray live monitor](https://raw.githubusercontent.com/theowecker/txray/main/doc/monitor.png)
234
296
 
235
- transactions 142 seen 9 slow 4 with findings
236
- duration p50 12ms p95 240ms max 1.84s
237
- ▁▁▂▁▁▁▁▁▁▂▁▁▁▁▁▅▁▁▁▁▁▂▁▁▁▁▁▁▁▁▂█
297
+ The `txns` meter is a stacked bar of clean, slow and flagged transactions. The `time` row is a log scaled sparkline of recent durations next to p50, p95 and max, all coloured against your threshold. Below that is a latency histogram, then the live feed, where findings hang under the transaction that produced them, so you see the 1.84 second transaction and the two calls that account for most of it. Hotspots rank what keeps happening. Everything is coloured by rule severity and clipped to your terminal, down to 60 columns.
238
298
 
239
- LIVE
240
- 14:02:11 1.84s x app/models/order.rb:31 settle
241
- | http-in-transaction POST api.stripe.com/v1/charges (1.61s)
242
- | mail-in-transaction ReceiptMailer delivered mail (180ms)
243
- 14:02:10 268ms ! app/services/checkout.rb:12 call
244
- 14:02:09 11ms . app/models/photo.rb:8 save
299
+ ### Turning it on
245
300
 
246
- HOTSPOTS
247
- 3x http-in-transaction app/models/order.rb:34
248
- 1x job-enqueue-in-transaction app/models/photo.rb:8
301
+ Three steps, in this order:
249
302
 
250
- ctrl-c to stop
251
- ```
303
+ 1. **Gemfile** must not say `require: false` (see Install above), or nothing is ever written.
304
+ 2. **Enable the guard**, in `.txray.yml`:
252
305
 
253
- Findings are grouped under the transaction that produced them, so you see the transaction that took 1.84 seconds and the two calls that account for most of it. Ctrl-C prints a summary of the session. Piped to a file or another process it emits the raw event stream instead of the dashboard, so `txray watch | jq` works.
306
+ ```yaml
307
+ runtime:
308
+ enabled: true
309
+ threshold_ms: 250
310
+ ```
254
311
 
255
- The log is append only with an exclusive lock per write, so several Puma workers and Sidekiq processes can share one file.
312
+ or in `config/environments/development.rb`:
313
+
314
+ ```ruby
315
+ config.txray.enabled = true
316
+ config.txray.threshold_ms = 250
317
+ ```
318
+
319
+ 3. **Restart the app**, then run the two side by side:
320
+
321
+ ```
322
+ terminal 1: bin/dev
323
+ terminal 2: bundle exec txray watch
324
+ ```
325
+
326
+ Exercise the app and transactions appear as they close. Only transactions that ran past the threshold or carried a finding are recorded, so the feed stays quiet until something is actually wrong. If the log does not exist yet, `txray watch` says so and repeats these steps on screen rather than sitting blank.
327
+
328
+ | Flag | Effect |
329
+ | --- | --- |
330
+ | `--from-start` | replay the existing log instead of only new events |
331
+ | `--threshold 100` | lower the bar to see more |
332
+ | `--file PATH` | watch a different log |
333
+
334
+ Ctrl-C prints a summary of the session. Piped to a file or another process it emits the raw event stream instead of the dashboard, so `txray watch | jq` works.
335
+
336
+ The log is append only with an exclusive lock per write, so several Puma workers and Sidekiq processes can share one file. It is newline delimited JSON rather than a text log because the watcher reads it incrementally while it is being written: one line is one complete record, so a partial read is never a partial event, concurrent writers cannot interleave into one record, and the nested findings and durations survive without parsing prose. It is still a plain text file, so `tail -f tmp/txray.ndjson` works.
256
337
 
257
338
  ## How this differs from what already exists
258
339
 
@@ -269,6 +350,8 @@ bin/setup
269
350
  bundle exec rake
270
351
  ```
271
352
 
353
+ Releases are cut by tagging; see [CONTRIBUTING.md](CONTRIBUTING.md).
354
+
272
355
  ## License
273
356
 
274
357
  MIT
data/Rakefile CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "bundler/gem_tasks"
3
4
  require "rspec/core/rake_task"
4
5
  require "rubocop/rake_task"
5
6
 
data/doc/monitor.png ADDED
Binary file
data/doc/monitor.svg ADDED
@@ -0,0 +1,229 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="741" height="617" viewBox="0 0 741 617" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13">
2
+ <rect width="741" height="617" rx="8" fill="#16181d"/>
3
+ <rect x="14.0" y="13.5" width="713.0" height="19.0" fill="#3a3f4b"/>
4
+ <text x="14.0" y="27.0" xml:space="preserve" fill="#f5f6f8" font-weight="700"> txray tmp/txray.ndjson up 00:04:12 2 pids </text>
5
+ <text x="14.0" y="65.0" xml:space="preserve" fill="#c8ccd4"> </text>
6
+ <text x="29.5" y="65.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">txns </text>
7
+ <text x="76.0" y="65.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">[</text>
8
+ <text x="83.75" y="65.0" xml:space="preserve" fill="#5ddc9a">████████████████████████████████</text>
9
+ <text x="331.75" y="65.0" xml:space="preserve" fill="#e5c07b">█</text>
10
+ <text x="339.5" y="65.0" xml:space="preserve" fill="#ff6b6b">█</text>
11
+ <text x="347.25" y="65.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">]</text>
12
+ <text x="355.0" y="65.0" xml:space="preserve" fill="#c8ccd4"> 123 txns</text>
13
+ <text x="432.5" y="65.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62"> · </text>
14
+ <text x="455.75" y="65.0" xml:space="preserve" fill="#5ddc9a">120 ok</text>
15
+ <text x="502.25" y="65.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62"> · </text>
16
+ <text x="525.5" y="65.0" xml:space="preserve" fill="#e5c07b">1 slow</text>
17
+ <text x="572.0" y="65.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62"> · </text>
18
+ <text x="595.25" y="65.0" xml:space="preserve" fill="#ff6b6b">2 flagged</text>
19
+ <text x="14.0" y="84.0" xml:space="preserve" fill="#c8ccd4"> </text>
20
+ <text x="29.5" y="84.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">time </text>
21
+ <text x="76.0" y="84.0" xml:space="preserve" fill="#5ddc9a">▃</text>
22
+ <text x="83.75" y="84.0" xml:space="preserve" fill="#5ddc9a">▃</text>
23
+ <text x="91.5" y="84.0" xml:space="preserve" fill="#5ddc9a">▄</text>
24
+ <text x="99.25" y="84.0" xml:space="preserve" fill="#5ddc9a">▅</text>
25
+ <text x="107.0" y="84.0" xml:space="preserve" fill="#5ddc9a">▃</text>
26
+ <text x="114.75" y="84.0" xml:space="preserve" fill="#5ddc9a">▄</text>
27
+ <text x="122.5" y="84.0" xml:space="preserve" fill="#5ddc9a">▃</text>
28
+ <text x="130.25" y="84.0" xml:space="preserve" fill="#5ddc9a">▃</text>
29
+ <text x="138.0" y="84.0" xml:space="preserve" fill="#5ddc9a">▃</text>
30
+ <text x="145.75" y="84.0" xml:space="preserve" fill="#5ddc9a">▄</text>
31
+ <text x="153.5" y="84.0" xml:space="preserve" fill="#5ddc9a">▅</text>
32
+ <text x="161.25" y="84.0" xml:space="preserve" fill="#5ddc9a">▃</text>
33
+ <text x="169.0" y="84.0" xml:space="preserve" fill="#5ddc9a">▃</text>
34
+ <text x="176.75" y="84.0" xml:space="preserve" fill="#5ddc9a">▅</text>
35
+ <text x="184.5" y="84.0" xml:space="preserve" fill="#5ddc9a">▄</text>
36
+ <text x="192.25" y="84.0" xml:space="preserve" fill="#5ddc9a">▅</text>
37
+ <text x="200.0" y="84.0" xml:space="preserve" fill="#5ddc9a">▄</text>
38
+ <text x="207.75" y="84.0" xml:space="preserve" fill="#5ddc9a">▄</text>
39
+ <text x="215.5" y="84.0" xml:space="preserve" fill="#5ddc9a">▃</text>
40
+ <text x="223.25" y="84.0" xml:space="preserve" fill="#5ddc9a">▄</text>
41
+ <text x="231.0" y="84.0" xml:space="preserve" fill="#5ddc9a">▅</text>
42
+ <text x="238.75" y="84.0" xml:space="preserve" fill="#5ddc9a">▂</text>
43
+ <text x="246.5" y="84.0" xml:space="preserve" fill="#5ddc9a">▃</text>
44
+ <text x="254.25" y="84.0" xml:space="preserve" fill="#5ddc9a">▂</text>
45
+ <text x="262.0" y="84.0" xml:space="preserve" fill="#5ddc9a">▄</text>
46
+ <text x="269.75" y="84.0" xml:space="preserve" fill="#5ddc9a">▅</text>
47
+ <text x="277.5" y="84.0" xml:space="preserve" fill="#5ddc9a">▄</text>
48
+ <text x="285.25" y="84.0" xml:space="preserve" fill="#5ddc9a">▄</text>
49
+ <text x="293.0" y="84.0" xml:space="preserve" fill="#5ddc9a">▃</text>
50
+ <text x="300.75" y="84.0" xml:space="preserve" fill="#5ddc9a">▄</text>
51
+ <text x="308.5" y="84.0" xml:space="preserve" fill="#5ddc9a">▃</text>
52
+ <text x="316.25" y="84.0" xml:space="preserve" fill="#ff6b6b">▆</text>
53
+ <text x="324.0" y="84.0" xml:space="preserve" fill="#ff6b6b">▇</text>
54
+ <text x="331.75" y="84.0" xml:space="preserve" fill="#ff6b6b">█</text>
55
+ <text x="339.5" y="84.0" xml:space="preserve" fill="#c8ccd4"> </text>
56
+ <text x="355.0" y="84.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">p50</text>
57
+ <text x="378.25" y="84.0" xml:space="preserve" fill="#c8ccd4"> </text>
58
+ <text x="386.0" y="84.0" xml:space="preserve" fill="#5ddc9a">18ms</text>
59
+ <text x="417.0" y="84.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62"> · </text>
60
+ <text x="440.25" y="84.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">p95</text>
61
+ <text x="463.5" y="84.0" xml:space="preserve" fill="#c8ccd4"> </text>
62
+ <text x="471.25" y="84.0" xml:space="preserve" fill="#e5c07b">140ms</text>
63
+ <text x="510.0" y="84.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62"> · </text>
64
+ <text x="533.25" y="84.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">max</text>
65
+ <text x="556.5" y="84.0" xml:space="preserve" fill="#c8ccd4"> </text>
66
+ <text x="564.25" y="84.0" xml:space="preserve" fill="#ff6b6b">1.84s</text>
67
+ <text x="14.0" y="122.0" xml:space="preserve" fill="#c8ccd4"> </text>
68
+ <text x="29.5" y="122.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62"> &lt;10ms</text>
69
+ <text x="83.75" y="122.0" xml:space="preserve" fill="#c8ccd4"> </text>
70
+ <text x="91.5" y="122.0" xml:space="preserve" fill="#5ddc9a">███████████████████████████████</text>
71
+ <text x="331.75" y="122.0" xml:space="preserve" fill="#c8ccd4"> </text>
72
+ <text x="339.5" y="122.0" xml:space="preserve" fill="#f0f0f0">45 </text>
73
+ <text x="370.5" y="122.0" xml:space="preserve" fill="#c8ccd4"> </text>
74
+ <text x="386.0" y="122.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62"> &lt;50ms</text>
75
+ <text x="440.25" y="122.0" xml:space="preserve" fill="#c8ccd4"> </text>
76
+ <text x="448.0" y="122.0" xml:space="preserve" fill="#5ddc9a">█████████████████████████████</text>
77
+ <text x="672.75" y="122.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">··</text>
78
+ <text x="688.25" y="122.0" xml:space="preserve" fill="#c8ccd4"> </text>
79
+ <text x="696.0" y="122.0" xml:space="preserve" fill="#f0f0f0">42 </text>
80
+ <text x="14.0" y="141.0" xml:space="preserve" fill="#c8ccd4"> </text>
81
+ <text x="29.5" y="141.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62"> &lt;100ms</text>
82
+ <text x="83.75" y="141.0" xml:space="preserve" fill="#c8ccd4"> </text>
83
+ <text x="91.5" y="141.0" xml:space="preserve" fill="#5ddc9a">███████████████████</text>
84
+ <text x="238.75" y="141.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">············</text>
85
+ <text x="331.75" y="141.0" xml:space="preserve" fill="#c8ccd4"> </text>
86
+ <text x="339.5" y="141.0" xml:space="preserve" fill="#f0f0f0">27 </text>
87
+ <text x="370.5" y="141.0" xml:space="preserve" fill="#c8ccd4"> </text>
88
+ <text x="386.0" y="141.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62"> &lt;250ms</text>
89
+ <text x="440.25" y="141.0" xml:space="preserve" fill="#c8ccd4"> </text>
90
+ <text x="448.0" y="141.0" xml:space="preserve" fill="#e5c07b">████</text>
91
+ <text x="479.0" y="141.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">···························</text>
92
+ <text x="688.25" y="141.0" xml:space="preserve" fill="#c8ccd4"> </text>
93
+ <text x="696.0" y="141.0" xml:space="preserve" fill="#f0f0f0">6 </text>
94
+ <text x="14.0" y="160.0" xml:space="preserve" fill="#c8ccd4"> </text>
95
+ <text x="29.5" y="160.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62"> &lt;1s</text>
96
+ <text x="83.75" y="160.0" xml:space="preserve" fill="#c8ccd4"> </text>
97
+ <text x="91.5" y="160.0" xml:space="preserve" fill="#ff6b6b">█</text>
98
+ <text x="99.25" y="160.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">······························</text>
99
+ <text x="331.75" y="160.0" xml:space="preserve" fill="#c8ccd4"> </text>
100
+ <text x="339.5" y="160.0" xml:space="preserve" fill="#f0f0f0">2 </text>
101
+ <text x="370.5" y="160.0" xml:space="preserve" fill="#c8ccd4"> </text>
102
+ <text x="386.0" y="160.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62"> 1s+</text>
103
+ <text x="440.25" y="160.0" xml:space="preserve" fill="#c8ccd4"> </text>
104
+ <text x="448.0" y="160.0" xml:space="preserve" fill="#ff6b6b">█</text>
105
+ <text x="455.75" y="160.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">······························</text>
106
+ <text x="688.25" y="160.0" xml:space="preserve" fill="#c8ccd4"> </text>
107
+ <text x="696.0" y="160.0" xml:space="preserve" fill="#f0f0f0">1 </text>
108
+ <rect x="14.0" y="184.5" width="713.0" height="19.0" fill="#3a3f4b"/>
109
+ <text x="14.0" y="198.0" xml:space="preserve" fill="#f5f6f8" font-weight="700"> TIME ELAPSED SOURCE </text>
110
+ <text x="14.0" y="217.0" xml:space="preserve" fill="#c8ccd4"> </text>
111
+ <text x="29.5" y="217.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">14:12:13</text>
112
+ <text x="91.5" y="217.0" xml:space="preserve" fill="#c8ccd4"> </text>
113
+ <text x="99.25" y="217.0" xml:space="preserve" fill="#ff6b6b"> 1.84s</text>
114
+ <text x="169.0" y="217.0" xml:space="preserve" fill="#c8ccd4"> </text>
115
+ <text x="176.75" y="217.0" xml:space="preserve" fill="#ff6b6b">●</text>
116
+ <text x="184.5" y="217.0" xml:space="preserve" fill="#c8ccd4"> app/models/order.rb:31 settle</text>
117
+ <text x="14.0" y="236.0" xml:space="preserve" fill="#c8ccd4"> </text>
118
+ <text x="184.5" y="236.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">└</text>
119
+ <text x="192.25" y="236.0" xml:space="preserve" fill="#c8ccd4"> </text>
120
+ <text x="200.0" y="236.0" xml:space="preserve" fill="#ff6b6b">http-in-transaction</text>
121
+ <text x="347.25" y="236.0" xml:space="preserve" fill="#c8ccd4"> </text>
122
+ <text x="355.0" y="236.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">POST api.stripe.com/v1/charges (1.61s)</text>
123
+ <text x="14.0" y="255.0" xml:space="preserve" fill="#c8ccd4"> </text>
124
+ <text x="184.5" y="255.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">└</text>
125
+ <text x="192.25" y="255.0" xml:space="preserve" fill="#c8ccd4"> </text>
126
+ <text x="200.0" y="255.0" xml:space="preserve" fill="#e5c07b">job-enqueue-in-transaction</text>
127
+ <text x="401.5" y="255.0" xml:space="preserve" fill="#c8ccd4"> </text>
128
+ <text x="409.25" y="255.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">ShipJob enqueued</text>
129
+ <text x="14.0" y="274.0" xml:space="preserve" fill="#c8ccd4"> </text>
130
+ <text x="29.5" y="274.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">14:12:04</text>
131
+ <text x="91.5" y="274.0" xml:space="preserve" fill="#c8ccd4"> </text>
132
+ <text x="99.25" y="274.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62"> -</text>
133
+ <text x="169.0" y="274.0" xml:space="preserve" fill="#c8ccd4"> </text>
134
+ <text x="176.75" y="274.0" xml:space="preserve" fill="#56b6c2">●</text>
135
+ <text x="184.5" y="274.0" xml:space="preserve" fill="#c8ccd4"> </text>
136
+ <text x="192.25" y="274.0" xml:space="preserve" fill="#56b6c2">cache-in-transaction</text>
137
+ <text x="347.25" y="274.0" xml:space="preserve" fill="#c8ccd4"> </text>
138
+ <text x="355.0" y="274.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">app/models/album.rb:19</text>
139
+ <text x="14.0" y="293.0" xml:space="preserve" fill="#c8ccd4"> </text>
140
+ <text x="29.5" y="293.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">14:11:54</text>
141
+ <text x="91.5" y="293.0" xml:space="preserve" fill="#c8ccd4"> </text>
142
+ <text x="99.25" y="293.0" xml:space="preserve" fill="#ff6b6b"> 612ms</text>
143
+ <text x="169.0" y="293.0" xml:space="preserve" fill="#c8ccd4"> </text>
144
+ <text x="176.75" y="293.0" xml:space="preserve" fill="#ff6b6b">●</text>
145
+ <text x="184.5" y="293.0" xml:space="preserve" fill="#c8ccd4"> app/models/photo.rb:44 attach_original</text>
146
+ <text x="14.0" y="312.0" xml:space="preserve" fill="#c8ccd4"> </text>
147
+ <text x="184.5" y="312.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">└</text>
148
+ <text x="192.25" y="312.0" xml:space="preserve" fill="#c8ccd4"> </text>
149
+ <text x="200.0" y="312.0" xml:space="preserve" fill="#e5c07b">upload-in-transaction</text>
150
+ <text x="362.75" y="312.0" xml:space="preserve" fill="#c8ccd4"> </text>
151
+ <text x="370.5" y="312.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">original.attach(io: file) (540ms)</text>
152
+ <text x="14.0" y="331.0" xml:space="preserve" fill="#c8ccd4"> </text>
153
+ <text x="29.5" y="331.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">14:11:36</text>
154
+ <text x="91.5" y="331.0" xml:space="preserve" fill="#c8ccd4"> </text>
155
+ <text x="99.25" y="331.0" xml:space="preserve" fill="#ff6b6b"> 268ms</text>
156
+ <text x="169.0" y="331.0" xml:space="preserve" fill="#c8ccd4"> </text>
157
+ <text x="176.75" y="331.0" xml:space="preserve" fill="#e5c07b">●</text>
158
+ <text x="184.5" y="331.0" xml:space="preserve" fill="#c8ccd4"> app/services/checkout.rb:12 call</text>
159
+ <text x="14.0" y="350.0" xml:space="preserve" fill="#c8ccd4"> </text>
160
+ <text x="29.5" y="350.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">14:09:15</text>
161
+ <text x="91.5" y="350.0" xml:space="preserve" fill="#c8ccd4"> </text>
162
+ <text x="99.25" y="350.0" xml:space="preserve" fill="#5ddc9a"> 9ms</text>
163
+ <text x="169.0" y="350.0" xml:space="preserve" fill="#c8ccd4"> </text>
164
+ <text x="176.75" y="350.0" xml:space="preserve" fill="#5ddc9a">·</text>
165
+ <text x="184.5" y="350.0" xml:space="preserve" fill="#c8ccd4"> app/models/photo.rb:8 save</text>
166
+ <text x="14.0" y="369.0" xml:space="preserve" fill="#c8ccd4"> </text>
167
+ <text x="29.5" y="369.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">14:09:14</text>
168
+ <text x="91.5" y="369.0" xml:space="preserve" fill="#c8ccd4"> </text>
169
+ <text x="99.25" y="369.0" xml:space="preserve" fill="#5ddc9a"> 18ms</text>
170
+ <text x="169.0" y="369.0" xml:space="preserve" fill="#c8ccd4"> </text>
171
+ <text x="176.75" y="369.0" xml:space="preserve" fill="#5ddc9a">·</text>
172
+ <text x="184.5" y="369.0" xml:space="preserve" fill="#c8ccd4"> app/models/photo.rb:8 save</text>
173
+ <text x="14.0" y="388.0" xml:space="preserve" fill="#c8ccd4"> </text>
174
+ <text x="29.5" y="388.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">14:09:13</text>
175
+ <text x="91.5" y="388.0" xml:space="preserve" fill="#c8ccd4"> </text>
176
+ <text x="99.25" y="388.0" xml:space="preserve" fill="#5ddc9a"> 9ms</text>
177
+ <text x="169.0" y="388.0" xml:space="preserve" fill="#c8ccd4"> </text>
178
+ <text x="176.75" y="388.0" xml:space="preserve" fill="#5ddc9a">·</text>
179
+ <text x="184.5" y="388.0" xml:space="preserve" fill="#c8ccd4"> app/models/photo.rb:8 save</text>
180
+ <text x="14.0" y="407.0" xml:space="preserve" fill="#c8ccd4"> </text>
181
+ <text x="29.5" y="407.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">14:09:12</text>
182
+ <text x="91.5" y="407.0" xml:space="preserve" fill="#c8ccd4"> </text>
183
+ <text x="99.25" y="407.0" xml:space="preserve" fill="#5ddc9a"> 41ms</text>
184
+ <text x="169.0" y="407.0" xml:space="preserve" fill="#c8ccd4"> </text>
185
+ <text x="176.75" y="407.0" xml:space="preserve" fill="#5ddc9a">·</text>
186
+ <text x="184.5" y="407.0" xml:space="preserve" fill="#c8ccd4"> app/models/photo.rb:8 save</text>
187
+ <text x="14.0" y="426.0" xml:space="preserve" fill="#c8ccd4"> </text>
188
+ <text x="29.5" y="426.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">14:09:11</text>
189
+ <text x="91.5" y="426.0" xml:space="preserve" fill="#c8ccd4"> </text>
190
+ <text x="99.25" y="426.0" xml:space="preserve" fill="#5ddc9a"> 18ms</text>
191
+ <text x="169.0" y="426.0" xml:space="preserve" fill="#c8ccd4"> </text>
192
+ <text x="176.75" y="426.0" xml:space="preserve" fill="#5ddc9a">·</text>
193
+ <text x="184.5" y="426.0" xml:space="preserve" fill="#c8ccd4"> app/models/photo.rb:8 save</text>
194
+ <text x="14.0" y="445.0" xml:space="preserve" fill="#c8ccd4"> </text>
195
+ <text x="29.5" y="445.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">14:09:10</text>
196
+ <text x="91.5" y="445.0" xml:space="preserve" fill="#c8ccd4"> </text>
197
+ <text x="99.25" y="445.0" xml:space="preserve" fill="#5ddc9a"> 63ms</text>
198
+ <text x="169.0" y="445.0" xml:space="preserve" fill="#c8ccd4"> </text>
199
+ <text x="176.75" y="445.0" xml:space="preserve" fill="#5ddc9a">·</text>
200
+ <text x="184.5" y="445.0" xml:space="preserve" fill="#c8ccd4"> app/models/photo.rb:8 save</text>
201
+ <rect x="14.0" y="469.5" width="713.0" height="19.0" fill="#3a3f4b"/>
202
+ <text x="14.0" y="483.0" xml:space="preserve" fill="#f5f6f8" font-weight="700"> HOTSPOTS </text>
203
+ <text x="14.0" y="502.0" xml:space="preserve" fill="#c8ccd4"> </text>
204
+ <text x="29.5" y="502.0" xml:space="preserve" fill="#f0f0f0"> 1x</text>
205
+ <text x="68.25" y="502.0" xml:space="preserve" fill="#c8ccd4"> </text>
206
+ <text x="83.75" y="502.0" xml:space="preserve" fill="#56b6c2">cache-in-transaction </text>
207
+ <text x="331.75" y="502.0" xml:space="preserve" fill="#c8ccd4"> </text>
208
+ <text x="339.5" y="502.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">app/models/album.rb:19</text>
209
+ <text x="14.0" y="521.0" xml:space="preserve" fill="#c8ccd4"> </text>
210
+ <text x="29.5" y="521.0" xml:space="preserve" fill="#f0f0f0"> 1x</text>
211
+ <text x="68.25" y="521.0" xml:space="preserve" fill="#c8ccd4"> </text>
212
+ <text x="83.75" y="521.0" xml:space="preserve" fill="#e5c07b">upload-in-transaction </text>
213
+ <text x="331.75" y="521.0" xml:space="preserve" fill="#c8ccd4"> </text>
214
+ <text x="339.5" y="521.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">app/models/photo.rb:46</text>
215
+ <text x="14.0" y="540.0" xml:space="preserve" fill="#c8ccd4"> </text>
216
+ <text x="29.5" y="540.0" xml:space="preserve" fill="#f0f0f0"> 1x</text>
217
+ <text x="68.25" y="540.0" xml:space="preserve" fill="#c8ccd4"> </text>
218
+ <text x="83.75" y="540.0" xml:space="preserve" fill="#ff6b6b">http-in-transaction </text>
219
+ <text x="331.75" y="540.0" xml:space="preserve" fill="#c8ccd4"> </text>
220
+ <text x="339.5" y="540.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">app/models/order.rb:34</text>
221
+ <text x="14.0" y="559.0" xml:space="preserve" fill="#c8ccd4"> </text>
222
+ <text x="29.5" y="559.0" xml:space="preserve" fill="#f0f0f0"> 1x</text>
223
+ <text x="68.25" y="559.0" xml:space="preserve" fill="#c8ccd4"> </text>
224
+ <text x="83.75" y="559.0" xml:space="preserve" fill="#e5c07b">job-enqueue-in-transaction </text>
225
+ <text x="331.75" y="559.0" xml:space="preserve" fill="#c8ccd4"> </text>
226
+ <text x="339.5" y="559.0" xml:space="preserve" fill="#c8ccd4" opacity="0.62">app/models/order.rb:38</text>
227
+ <rect x="14.0" y="583.5" width="713.0" height="19.0" fill="#3a3f4b"/>
228
+ <text x="14.0" y="597.0" xml:space="preserve" fill="#f5f6f8" font-weight="700"> ctrl-c stop </text>
229
+ </svg>
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module Txray
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ desc "Writes .txray.yml and explains how to turn the runtime guard on."
11
+
12
+ def create_config
13
+ copy_file "txray.yml", ".txray.yml"
14
+ end
15
+
16
+ def explain
17
+ say ""
18
+ say "Scan the application:", :green
19
+ say " bundle exec txray"
20
+ say ""
21
+ say "To watch transactions live, set runtime.enabled to true in .txray.yml,", :green
22
+ say "make sure the Gemfile does not say require: false, restart, then run:"
23
+ say " bundle exec txray watch"
24
+ say ""
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,37 @@
1
+ # Where to look. Paths are relative to the project root.
2
+ include:
3
+ - app
4
+ - lib
5
+ - db/migrate
6
+ exclude:
7
+ - spec
8
+ - test
9
+ - vendor
10
+
11
+ # How far to follow a call out of a transaction before giving up.
12
+ max_depth: 3
13
+
14
+ # Exit non zero at or above this severity: low, medium, high or none.
15
+ fail_level: low
16
+
17
+ # Rules to turn off entirely. `txray --rules` lists them all.
18
+ disabled_rules: []
19
+
20
+ # Raise or lower a rule without turning it off.
21
+ severities: {}
22
+
23
+ # Your own service wrappers. Any constant under these is treated as a
24
+ # third party client, so a call on one inside a transaction is reported.
25
+ external_clients: []
26
+
27
+ # The runtime guard. Off by default. It reports transactions that run long
28
+ # and the HTTP, mail and job work that happens while one is open, and writes
29
+ # the event log that `txray watch` reads.
30
+ #
31
+ # The gem must NOT be `require: false` in your Gemfile for this to load.
32
+ runtime:
33
+ enabled: false
34
+ threshold_ms: 250
35
+ on_violation: log # log, or raise to fail loudly in test
36
+ log_path: tmp/txray.ndjson
37
+ ignore: []
data/lib/txray/cli.rb CHANGED
@@ -63,6 +63,9 @@ module Txray
63
63
 
64
64
  File.write(Config::FILENAME, YAML.dump(Config::DEFAULTS))
65
65
  @io.puts "created #{Config::FILENAME}"
66
+ @io.puts
67
+ @io.puts "scan with: bundle exec txray"
68
+ @io.puts "watch transactions: set runtime.enabled to true, restart, then bundle exec txray watch"
66
69
  EXIT_CLEAN
67
70
  end
68
71