request_trail 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -1
- data/README.md +132 -9
- data/lib/generators/request_trail/install/templates/request_trail.rb.tt +1 -0
- data/lib/request_trail/active_job_subscriber.rb +38 -0
- data/lib/request_trail/collector.rb +9 -1
- data/lib/request_trail/configuration.rb +2 -1
- data/lib/request_trail/formatter.rb +13 -3
- data/lib/request_trail/formatters/base.rb +7 -0
- data/lib/request_trail/formatters/flame_graph.rb +8 -4
- data/lib/request_trail/formatters/json.rb +3 -1
- data/lib/request_trail/subscriber.rb +10 -1
- data/lib/request_trail/version.rb +1 -1
- data/lib/request_trail.rb +1 -0
- data/sig/request_trail/active_job_subscriber.rbs +15 -0
- data/sig/request_trail/collector.rbs +3 -0
- data/sig/request_trail/configuration.rbs +1 -0
- data/sig/request_trail/formatters/base.rbs +5 -1
- data/sig/request_trail/subscriber.rbs +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 46f0f0b5ac13bf9ea151c441b9de780076a0350c34a4268258701bd4bc9e2bd6
|
|
4
|
+
data.tar.gz: 0d1823f675d5074c2b1ee8664970737b81ac9a3b3535a58a1f56bdf029f5089e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2d1bf8caea4104913b8a4ef9c1b1c4aa517d4aaeb96f83f5fa49242ceee598cbdb6c118191e6bd0eecacd4ee3bdbaf77293f6725750768dff5dd4a182bfae6ee
|
|
7
|
+
data.tar.gz: c412309dd55a91d2eeec4c9cbed03999a1dc4e317638873ccb7662a384e146ee3af8d3a0f783fa9288047eca475d8cfc578048c54d556354e8fcf66d51a70575
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.7.0] - 2026-06-14
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `config.n_plus_one_threshold` — flag requests where SQL query count meets or exceeds a threshold; appends `[N+1?]` to the header line in plain-text and FlameGraph output, and sets `"n_plus_one": true` in JSON output (default: `nil` = disabled)
|
|
8
|
+
- `RequestTrail::ActiveJobSubscriber` — module to include in `ApplicationJob`; adds an `around_perform` hook that traces background job execution across all ActiveJob backends (GoodJob, Solid Queue, DelayedJob, Sidekiq-via-ActiveJob)
|
|
9
|
+
- Outbound HTTP tracking — `Subscriber` subscribes to `request.faraday` notifications; `http_count` and `http_duration_ms` are added to `Collector` and surfaced in all formatters (plain text, FlameGraph, JSON)
|
|
10
|
+
|
|
3
11
|
## [0.6.0] - 2026-06-14
|
|
4
12
|
|
|
5
13
|
### Added
|
|
@@ -58,7 +66,9 @@
|
|
|
58
66
|
- `RequestTrail::Subscriber` — attach/detach API for notification subscriptions
|
|
59
67
|
- `RequestTrail::Collector` — thread-safe per-request event accumulator
|
|
60
68
|
|
|
61
|
-
[Unreleased]: https://github.com/eclectic-coding/request-trail/compare/v0.
|
|
69
|
+
[Unreleased]: https://github.com/eclectic-coding/request-trail/compare/v0.7.0...HEAD
|
|
70
|
+
[0.7.0]: https://github.com/eclectic-coding/request-trail/compare/v0.6.0...v0.7.0
|
|
71
|
+
[0.6.0]: https://github.com/eclectic-coding/request-trail/compare/v0.5.0...v0.6.0
|
|
62
72
|
[0.5.0]: https://github.com/eclectic-coding/request-trail/releases/tag/v0.5.0
|
|
63
73
|
[0.4.0]: https://github.com/eclectic-coding/request-trail/releases/tag/v0.4.0
|
|
64
74
|
[0.3.0]: https://github.com/eclectic-coding/request-trail/releases/tag/v0.3.0
|
data/README.md
CHANGED
|
@@ -12,7 +12,18 @@ Middleware that traces a request through all the layers (middleware, controller,
|
|
|
12
12
|
|
|
13
13
|
- [Installation](#installation)
|
|
14
14
|
- [Usage](#usage)
|
|
15
|
-
- [
|
|
15
|
+
- [Rails](#rails)
|
|
16
|
+
- [JSON formatter](#json-formatter)
|
|
17
|
+
- [Flame graph formatter](#flame-graph-formatter)
|
|
18
|
+
- [N+1 detection](#n1-detection)
|
|
19
|
+
- [Rails log tags](#rails-log-tags)
|
|
20
|
+
- [Sidekiq](#sidekiq)
|
|
21
|
+
- [ActiveJob](#activejob)
|
|
22
|
+
- [Outbound HTTP tracking](#outbound-http-tracking)
|
|
23
|
+
- [Custom formatters](#custom-formatters)
|
|
24
|
+
- [Installation generator](#installation-generator)
|
|
25
|
+
- [Configuration](#configuration)
|
|
26
|
+
- [Non-Rails (plain Rack)](#non-rails-plain-rack)
|
|
16
27
|
- [Development](#development)
|
|
17
28
|
- [Contributing](#contributing)
|
|
18
29
|
- [License](#license)
|
|
@@ -110,6 +121,115 @@ RequestTrail::Formatters::FlameGraph.new(
|
|
|
110
121
|
|
|
111
122
|
Unspecified layers keep their defaults.
|
|
112
123
|
|
|
124
|
+
### N+1 detection
|
|
125
|
+
|
|
126
|
+
Set `config.n_plus_one_threshold` to flag requests that fire too many SQL queries. When `sql_count >= threshold`, all formatters add a prominent marker:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
RequestTrail.configure do |config|
|
|
130
|
+
config.n_plus_one_threshold = 10 # nil = disabled (default)
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Plain text and FlameGraph append `[N+1?]` to the header line:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
[RequestTrail] GET /orders 142ms [N+1?] | SQL: 15/85.0ms | Cache: 0 hits, 0 misses, 0.0ms
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
JSON includes `"n_plus_one": true` in the payload:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{"method":"GET","path":"/orders","duration_ms":142.5,"n_plus_one":true,"sql":{"count":15,"duration_ms":85.0},...}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Rails log tags
|
|
147
|
+
|
|
148
|
+
Add `RequestTrail.log_tag` to `config.log_tags` to include live request state on every tagged log line. The callable returns a lazy value that evaluates to `sql=N Nms` at log-line format time — so it reflects the accumulated query state at each point during the request:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
# config/application.rb
|
|
152
|
+
config.log_tags = [:request_id, RequestTrail.log_tag]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Example output:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
[abc-123] [sql=3 25ms] Completed 200 OK in 25ms
|
|
159
|
+
[abc-123] [sql=3 25ms] [RequestTrail] GET /orders 25ms | SQL: 3/10.0ms | Cache: 0 hits, 0 misses, 0.0ms
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The tag is omitted (blank-suppressed by Rails) on lines that fall outside an active request.
|
|
163
|
+
|
|
164
|
+
### Sidekiq
|
|
165
|
+
|
|
166
|
+
Add `RequestTrail::SidekiqMiddleware` to the Sidekiq server middleware chain to trace background job execution with the same flame-graph output:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
# config/initializers/sidekiq.rb
|
|
170
|
+
Sidekiq.configure_server do |config|
|
|
171
|
+
config.server_middleware do |chain|
|
|
172
|
+
chain.add RequestTrail::SidekiqMiddleware
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Output uses `JOB` as the method and includes the worker class and job ID:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
[RequestTrail] JOB HardWorker jid:abc123 42ms | SQL: 5/35.0ms | Cache: 0 hits, 0 misses, 0.0ms
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
All configuration options (`threshold_ms`, `sample_rate`, `n_plus_one_threshold`, `formatter`, etc.) apply to Sidekiq jobs the same way they apply to HTTP requests.
|
|
184
|
+
|
|
185
|
+
### ActiveJob
|
|
186
|
+
|
|
187
|
+
Include `RequestTrail::ActiveJobSubscriber` in `ApplicationJob` to trace background jobs across all ActiveJob backends (GoodJob, Solid Queue, DelayedJob, Sidekiq-via-ActiveJob, etc.):
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
# app/jobs/application_job.rb
|
|
191
|
+
class ApplicationJob < ActiveJob::Base
|
|
192
|
+
include RequestTrail::ActiveJobSubscriber
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
All jobs that inherit from `ApplicationJob` are then automatically traced. Output uses `JOB` as the method and includes the job class and `job_id`:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
[RequestTrail] JOB OrderSyncJob jid:476b61a6-ae85-4ae8-ae64-bf92e8cfeefc 42ms | SQL: 5/35.0ms | Cache: 0 hits, 0 misses, 0.0ms
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
All configuration options (`threshold_ms`, `sample_rate`, `n_plus_one_threshold`, `formatter`, etc.) apply to ActiveJob jobs the same way they apply to HTTP requests.
|
|
203
|
+
|
|
204
|
+
**`ActiveJobSubscriber` vs `SidekiqMiddleware`:** Use `ActiveJobSubscriber` when you want adapter-agnostic tracing via `ApplicationJob` inheritance. Use `SidekiqMiddleware` when running Sidekiq workers that do *not* go through ActiveJob (i.e., plain `Sidekiq::Worker` classes).
|
|
205
|
+
|
|
206
|
+
### Outbound HTTP tracking
|
|
207
|
+
|
|
208
|
+
Outbound HTTP calls made via Faraday are tracked automatically once `Subscriber.attach` is called (which happens via the Railtie in Rails apps). No additional configuration is needed — the `request.faraday` notification is subscribed alongside SQL and cache events.
|
|
209
|
+
|
|
210
|
+
Plain text output includes an `HTTP` segment:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
[RequestTrail] GET /orders 142ms | SQL: 7/38.3ms | Cache: 4 hits, 1 miss, 2.0ms | HTTP: 2/45.0ms
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Tiered output adds an `http` row under the controller:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
[RequestTrail] GET /orders 142ms
|
|
220
|
+
controller 104ms
|
|
221
|
+
sql 38ms (7 queries)
|
|
222
|
+
cache 2ms (4 hits, 1 miss)
|
|
223
|
+
view 22ms
|
|
224
|
+
http 45ms (2 calls)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
JSON output includes an `"http"` key:
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
{"method":"GET","path":"/orders","duration_ms":142.5,...,"http":{"count":2,"duration_ms":45.0}}
|
|
231
|
+
```
|
|
232
|
+
|
|
113
233
|
### Custom formatters
|
|
114
234
|
|
|
115
235
|
Any object that responds to `format(request, collector)` and returns a `String` can be used as a formatter. Include `RequestTrail::Formatters::Base` to make the contract explicit:
|
|
@@ -130,7 +250,7 @@ end
|
|
|
130
250
|
|
|
131
251
|
`format` receives:
|
|
132
252
|
- `request` — a `Rack::Request` with the current HTTP request
|
|
133
|
-
- `collector` — a `RequestTrail::Collector` exposing `elapsed_ms`, `sql_count`, `sql_duration_ms`, `cache_hits`, `cache_misses`, `cache_duration_ms`, `action_duration_ms`, and `
|
|
253
|
+
- `collector` — a `RequestTrail::Collector` exposing `elapsed_ms`, `sql_count`, `sql_duration_ms`, `cache_hits`, `cache_misses`, `cache_duration_ms`, `action_duration_ms`, `view_duration_ms`, `http_count`, and `http_duration_ms`
|
|
134
254
|
|
|
135
255
|
### Installation generator
|
|
136
256
|
|
|
@@ -149,17 +269,20 @@ Add an initializer to customize behavior:
|
|
|
149
269
|
```ruby
|
|
150
270
|
# config/initializers/request_trail.rb
|
|
151
271
|
RequestTrail.configure do |config|
|
|
152
|
-
config.enabled
|
|
153
|
-
config.log_level
|
|
154
|
-
config.threshold_ms
|
|
155
|
-
config.logger
|
|
156
|
-
config.formatter
|
|
272
|
+
config.enabled = true # set to false to disable entirely
|
|
273
|
+
config.log_level = :info # Rails logger level (:debug, :info, :warn)
|
|
274
|
+
config.threshold_ms = 200 # only log requests slower than this (0 = log all)
|
|
275
|
+
config.logger = nil # defaults to Rails.logger
|
|
276
|
+
config.formatter = RequestTrail::Formatters::FlameGraph.new # optional
|
|
157
277
|
|
|
158
278
|
# skip tracing for specific paths (strings = exact match, regexes = pattern match)
|
|
159
|
-
config.ignore_paths
|
|
279
|
+
config.ignore_paths = ["/health", "/up", /^\/assets/]
|
|
160
280
|
|
|
161
281
|
# trace only N% of requests — useful in high-traffic production environments
|
|
162
|
-
config.sample_rate
|
|
282
|
+
config.sample_rate = 0.1 # 0.0 = never, 1.0 = always (default)
|
|
283
|
+
|
|
284
|
+
# flag requests with too many SQL queries (nil = disabled)
|
|
285
|
+
config.n_plus_one_threshold = 10
|
|
163
286
|
end
|
|
164
287
|
```
|
|
165
288
|
|
|
@@ -6,6 +6,7 @@ RequestTrail.configure do |config|
|
|
|
6
6
|
config.threshold_ms = 0 # only log requests slower than this (0 = log all)
|
|
7
7
|
config.ignore_paths = [] # e.g. ["/health", /^\/assets/]
|
|
8
8
|
config.sample_rate = 1.0 # 0.0–1.0; 1.0 = trace every request
|
|
9
|
+
# config.n_plus_one_threshold = nil # warn when sql_count >= N (nil = disabled)
|
|
9
10
|
|
|
10
11
|
# config.logger = Rails.logger # defaults to Rails.logger
|
|
11
12
|
# config.formatter = RequestTrail::Formatters::FlameGraph.new(colorize: true)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RequestTrail
|
|
4
|
+
module ActiveJobSubscriber
|
|
5
|
+
JobContext = Struct.new(:request_method, :path, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
def self.included(base)
|
|
8
|
+
base.around_perform { |job, block| RequestTrail::ActiveJobSubscriber.trace(job, block) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.trace(job, block)
|
|
12
|
+
if RequestTrail.configuration.enabled && RequestTrail.configuration.sampled?
|
|
13
|
+
Collector.start
|
|
14
|
+
block.call
|
|
15
|
+
log(job)
|
|
16
|
+
else
|
|
17
|
+
block.call
|
|
18
|
+
end
|
|
19
|
+
ensure
|
|
20
|
+
Collector.stop
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.log(job)
|
|
24
|
+
collector = Collector.current
|
|
25
|
+
return if collector.elapsed_ms < RequestTrail.configuration.threshold_ms
|
|
26
|
+
|
|
27
|
+
message = RequestTrail.formatter.format(build_context(job), collector)
|
|
28
|
+
RequestTrail.configuration.logger.send(RequestTrail.configuration.log_level, message)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.build_context(job)
|
|
32
|
+
JobContext.new(
|
|
33
|
+
request_method: "JOB",
|
|
34
|
+
path: "#{job.class.name || job.class.to_s} jid:#{job.job_id}"
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -6,7 +6,8 @@ module RequestTrail
|
|
|
6
6
|
|
|
7
7
|
attr_reader :sql_count, :sql_duration_ms,
|
|
8
8
|
:cache_hits, :cache_misses, :cache_writes, :cache_duration_ms,
|
|
9
|
-
:action_duration_ms, :view_duration_ms
|
|
9
|
+
:action_duration_ms, :view_duration_ms,
|
|
10
|
+
:http_count, :http_duration_ms
|
|
10
11
|
|
|
11
12
|
def self.current
|
|
12
13
|
Thread.current[THREAD_KEY]
|
|
@@ -30,6 +31,8 @@ module RequestTrail
|
|
|
30
31
|
@cache_duration_ms = 0.0
|
|
31
32
|
@action_duration_ms = 0.0
|
|
32
33
|
@view_duration_ms = 0.0
|
|
34
|
+
@http_count = 0
|
|
35
|
+
@http_duration_ms = 0.0
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
def record_sql(duration_ms)
|
|
@@ -52,6 +55,11 @@ module RequestTrail
|
|
|
52
55
|
@view_duration_ms = view_duration_ms
|
|
53
56
|
end
|
|
54
57
|
|
|
58
|
+
def record_http(duration_ms:)
|
|
59
|
+
@http_count += 1
|
|
60
|
+
@http_duration_ms += duration_ms
|
|
61
|
+
end
|
|
62
|
+
|
|
55
63
|
def elapsed_ms
|
|
56
64
|
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at
|
|
57
65
|
(elapsed * 1000).round(2)
|
|
@@ -5,7 +5,7 @@ require "logger"
|
|
|
5
5
|
module RequestTrail
|
|
6
6
|
class Configuration
|
|
7
7
|
attr_writer :logger, :formatter
|
|
8
|
-
attr_accessor :enabled, :log_level, :threshold_ms, :ignore_paths, :sample_rate
|
|
8
|
+
attr_accessor :enabled, :log_level, :threshold_ms, :ignore_paths, :sample_rate, :n_plus_one_threshold
|
|
9
9
|
|
|
10
10
|
def initialize
|
|
11
11
|
@enabled = true
|
|
@@ -13,6 +13,7 @@ module RequestTrail
|
|
|
13
13
|
@threshold_ms = 0
|
|
14
14
|
@ignore_paths = []
|
|
15
15
|
@sample_rate = 1.0
|
|
16
|
+
@n_plus_one_threshold = nil
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def sampled?
|
|
@@ -5,10 +5,11 @@ module RequestTrail
|
|
|
5
5
|
include Formatters::Base
|
|
6
6
|
|
|
7
7
|
def format(request, collector)
|
|
8
|
-
|
|
8
|
+
n1 = n_plus_one?(collector) ? " [N+1?]" : ""
|
|
9
|
+
header = "[RequestTrail] #{request.request_method} #{request.path} #{collector.elapsed_ms.round}ms#{n1}"
|
|
9
10
|
return tiered_format(header, collector) if collector.action_duration_ms.positive?
|
|
10
11
|
|
|
11
|
-
"#{header} | #{sql_summary(collector)} | #{cache_summary(collector)}"
|
|
12
|
+
"#{header} | #{sql_summary(collector)} | #{cache_summary(collector)} | #{http_summary(collector)}"
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
private
|
|
@@ -19,7 +20,8 @@ module RequestTrail
|
|
|
19
20
|
" controller #{collector.action_duration_ms.round}ms",
|
|
20
21
|
" sql #{collector.sql_duration_ms.round(1)}ms (#{sql_count_label(collector)})",
|
|
21
22
|
" cache #{collector.cache_duration_ms.round(1)}ms (#{cache_detail(collector)})",
|
|
22
|
-
" view #{collector.view_duration_ms.round(1)}ms"
|
|
23
|
+
" view #{collector.view_duration_ms.round(1)}ms",
|
|
24
|
+
" http #{collector.http_duration_ms.round(1)}ms (#{http_count_label(collector)})"
|
|
23
25
|
].join("\n")
|
|
24
26
|
end
|
|
25
27
|
|
|
@@ -34,6 +36,14 @@ module RequestTrail
|
|
|
34
36
|
"Cache: #{collector.cache_hits} #{hit_label}, #{collector.cache_misses} #{miss_label}, #{duration}ms"
|
|
35
37
|
end
|
|
36
38
|
|
|
39
|
+
def http_summary(collector)
|
|
40
|
+
"HTTP: #{collector.http_count}/#{collector.http_duration_ms.round(1)}ms"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def http_count_label(collector)
|
|
44
|
+
collector.http_count == 1 ? "1 call" : "#{collector.http_count} calls"
|
|
45
|
+
end
|
|
46
|
+
|
|
37
47
|
def sql_count_label(collector)
|
|
38
48
|
collector.sql_count == 1 ? "1 query" : "#{collector.sql_count} queries"
|
|
39
49
|
end
|
|
@@ -12,6 +12,13 @@ module RequestTrail
|
|
|
12
12
|
def format(_request, _collector)
|
|
13
13
|
raise NotImplementedError, "#{self.class}#format must return a String"
|
|
14
14
|
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def n_plus_one?(collector)
|
|
19
|
+
threshold = RequestTrail.configuration.n_plus_one_threshold
|
|
20
|
+
threshold ? collector.sql_count >= threshold : false
|
|
21
|
+
end
|
|
15
22
|
end
|
|
16
23
|
end
|
|
17
24
|
end
|
|
@@ -13,7 +13,8 @@ module RequestTrail
|
|
|
13
13
|
controller: "\e[34m",
|
|
14
14
|
sql: "\e[33m",
|
|
15
15
|
cache: "\e[32m",
|
|
16
|
-
view: "\e[35m"
|
|
16
|
+
view: "\e[35m",
|
|
17
|
+
http: "\e[36m"
|
|
17
18
|
}.freeze
|
|
18
19
|
RESET = "\e[0m"
|
|
19
20
|
|
|
@@ -41,21 +42,24 @@ module RequestTrail
|
|
|
41
42
|
row(" ", "controller", collector.action_duration_ms, total, :controller),
|
|
42
43
|
row(" ", "sql", collector.sql_duration_ms, total, :sql),
|
|
43
44
|
row(" ", "cache", collector.cache_duration_ms, total, :cache),
|
|
44
|
-
row(" ", "view", collector.view_duration_ms, total, :view)
|
|
45
|
+
row(" ", "view", collector.view_duration_ms, total, :view),
|
|
46
|
+
row(" ", "http", collector.http_duration_ms, total, :http)
|
|
45
47
|
]
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
def flat_rows(collector, total)
|
|
49
51
|
[
|
|
50
52
|
row(" ", "sql", collector.sql_duration_ms, total, :sql),
|
|
51
|
-
row(" ", "cache", collector.cache_duration_ms, total, :cache)
|
|
53
|
+
row(" ", "cache", collector.cache_duration_ms, total, :cache),
|
|
54
|
+
row(" ", "http", collector.http_duration_ms, total, :http)
|
|
52
55
|
]
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
def header_line(request, collector)
|
|
56
59
|
elapsed = collector.elapsed_ms.round
|
|
57
60
|
bar = BAR_CHAR * BAR_WIDTH
|
|
58
|
-
|
|
61
|
+
n1 = n_plus_one?(collector) ? " [N+1?]" : ""
|
|
62
|
+
line = "[RequestTrail] #{request.request_method} #{request.path} #{elapsed}ms#{n1} #{bar}"
|
|
59
63
|
return line unless colorize?
|
|
60
64
|
|
|
61
65
|
"#{@colors[:header]}#{line}#{RESET}"
|
|
@@ -20,8 +20,10 @@ module RequestTrail
|
|
|
20
20
|
method: request.request_method,
|
|
21
21
|
path: request.path,
|
|
22
22
|
duration_ms: collector.elapsed_ms,
|
|
23
|
+
n_plus_one: n_plus_one?(collector),
|
|
23
24
|
sql: { count: collector.sql_count, duration_ms: collector.sql_duration_ms },
|
|
24
|
-
cache: cache_payload(collector)
|
|
25
|
+
cache: cache_payload(collector),
|
|
26
|
+
http: { count: collector.http_count, duration_ms: collector.http_duration_ms }
|
|
25
27
|
}
|
|
26
28
|
end
|
|
27
29
|
|
|
@@ -6,9 +6,11 @@ module RequestTrail
|
|
|
6
6
|
CACHE_READ_EVENT = "cache_read.active_support"
|
|
7
7
|
CACHE_WRITE_EVENT = "cache_write.active_support"
|
|
8
8
|
ACTION_EVENT = "process_action.action_controller"
|
|
9
|
+
HTTP_EVENT = "request.faraday"
|
|
9
10
|
|
|
10
11
|
def self.attach
|
|
11
|
-
@attach ||= [sql_subscription, cache_read_subscription, cache_write_subscription,
|
|
12
|
+
@attach ||= [sql_subscription, cache_read_subscription, cache_write_subscription,
|
|
13
|
+
action_subscription, http_subscription]
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def self.detach
|
|
@@ -51,5 +53,12 @@ module RequestTrail
|
|
|
51
53
|
)
|
|
52
54
|
end
|
|
53
55
|
end
|
|
56
|
+
|
|
57
|
+
private_class_method def self.http_subscription
|
|
58
|
+
ActiveSupport::Notifications.subscribe(HTTP_EVENT) do |*args|
|
|
59
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
60
|
+
Collector.current&.record_http(duration_ms: event.duration)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
54
63
|
end
|
|
55
64
|
end
|
data/lib/request_trail.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative "request_trail/formatters/flame_graph"
|
|
|
10
10
|
require_relative "request_trail/formatters/json"
|
|
11
11
|
require_relative "request_trail/log_tag"
|
|
12
12
|
require_relative "request_trail/middleware"
|
|
13
|
+
require_relative "request_trail/active_job_subscriber" if defined?(ActiveJob)
|
|
13
14
|
require_relative "request_trail/sidekiq_middleware" if defined?(Sidekiq)
|
|
14
15
|
require_relative "request_trail/railtie" if defined?(Rails::Railtie)
|
|
15
16
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module RequestTrail
|
|
2
|
+
module ActiveJobSubscriber
|
|
3
|
+
class JobContext
|
|
4
|
+
attr_accessor request_method: String
|
|
5
|
+
attr_accessor path: String
|
|
6
|
+
|
|
7
|
+
def initialize: (request_method: String, path: String) -> void
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.included: (Module base) -> void
|
|
11
|
+
def self.trace: (untyped job, ^() -> void block) -> void
|
|
12
|
+
def self.log: (untyped job) -> void
|
|
13
|
+
def self.build_context: (untyped job) -> JobContext
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -20,6 +20,8 @@ module RequestTrail
|
|
|
20
20
|
attr_reader cache_duration_ms: Float
|
|
21
21
|
attr_reader action_duration_ms: Float
|
|
22
22
|
attr_reader view_duration_ms: Float
|
|
23
|
+
attr_reader http_count: Integer
|
|
24
|
+
attr_reader http_duration_ms: Float
|
|
23
25
|
|
|
24
26
|
def self.current: () -> Collector?
|
|
25
27
|
def self.start: () -> Collector
|
|
@@ -30,6 +32,7 @@ module RequestTrail
|
|
|
30
32
|
def record_cache_read: (hit: bool, duration_ms: Float) -> Float
|
|
31
33
|
def record_cache_write: (duration_ms: Float) -> Float
|
|
32
34
|
def record_action: (duration_ms: Float, view_duration_ms: Float) -> Float
|
|
35
|
+
def record_http: (duration_ms: Float) -> Float
|
|
33
36
|
def elapsed_ms: () -> (Float | Integer)
|
|
34
37
|
end
|
|
35
38
|
end
|
|
@@ -4,6 +4,7 @@ module RequestTrail
|
|
|
4
4
|
CACHE_READ_EVENT: String
|
|
5
5
|
CACHE_WRITE_EVENT: String
|
|
6
6
|
ACTION_EVENT: String
|
|
7
|
+
HTTP_EVENT: String
|
|
7
8
|
|
|
8
9
|
self.@attach: Array[untyped]?
|
|
9
10
|
|
|
@@ -16,5 +17,6 @@ module RequestTrail
|
|
|
16
17
|
def self.cache_read_subscription: () -> untyped
|
|
17
18
|
def self.cache_write_subscription: () -> untyped
|
|
18
19
|
def self.action_subscription: () -> untyped
|
|
20
|
+
def self.http_subscription: () -> untyped
|
|
19
21
|
end
|
|
20
22
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: request_trail
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -57,6 +57,7 @@ files:
|
|
|
57
57
|
- lib/generators/request_trail/install/install_generator.rb
|
|
58
58
|
- lib/generators/request_trail/install/templates/request_trail.rb.tt
|
|
59
59
|
- lib/request_trail.rb
|
|
60
|
+
- lib/request_trail/active_job_subscriber.rb
|
|
60
61
|
- lib/request_trail/collector.rb
|
|
61
62
|
- lib/request_trail/configuration.rb
|
|
62
63
|
- lib/request_trail/formatter.rb
|
|
@@ -70,6 +71,7 @@ files:
|
|
|
70
71
|
- lib/request_trail/subscriber.rb
|
|
71
72
|
- lib/request_trail/version.rb
|
|
72
73
|
- sig/request_trail.rbs
|
|
74
|
+
- sig/request_trail/active_job_subscriber.rbs
|
|
73
75
|
- sig/request_trail/collector.rbs
|
|
74
76
|
- sig/request_trail/configuration.rbs
|
|
75
77
|
- sig/request_trail/formatter.rbs
|