request_trail 0.2.0 → 0.3.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 +16 -1
- data/README.md +48 -8
- data/ROADMAP.md +1 -12
- data/lib/request_trail/collector.rb +9 -1
- data/lib/request_trail/formatter.rb +24 -2
- data/lib/request_trail/subscriber.rb +12 -1
- data/lib/request_trail/version.rb +1 -1
- data/sig/request_trail/collector.rbs +5 -0
- data/sig/request_trail/formatter.rbs +8 -0
- data/sig/request_trail/subscriber.rbs +2 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 954b1805423f92d59582e41e86d55ac489577ef5c76d20701c0fbcd76956f2a8
|
|
4
|
+
data.tar.gz: 8464e352949c6eb14636ead83e5f0a556b8eb7dcabfd35186f35b3749ae8ac0a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c75f6813431fe8922454592a7dfb170fbddab4258a696901e79d0e1ec56699834cd7d9d0200d8eef1a7a3067dc95988e7bd0419b66b0ee6566bf0f342301dfd
|
|
7
|
+
data.tar.gz: 7054bee0d5f25f35513c9697bfe5000128d7932df335beae13acaeb43af5522fde160c045554526a505efacbe326d9892689756b4ea8b289d04869d7c73fc565
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-06-12
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Controller and view tracing via `process_action.action_controller` — records total controller duration and view runtime per request
|
|
8
|
+
- Formatter switches to tiered multi-line output when controller data is present:
|
|
9
|
+
```
|
|
10
|
+
[RequestTrail] GET /orders 142ms
|
|
11
|
+
controller 104ms
|
|
12
|
+
sql 38ms (7 queries)
|
|
13
|
+
cache 2ms (4 hits, 1 miss)
|
|
14
|
+
view 22ms
|
|
15
|
+
```
|
|
16
|
+
|
|
3
17
|
## [0.2.0] - 2026-06-11
|
|
4
18
|
|
|
5
19
|
### Added
|
|
@@ -19,6 +33,7 @@
|
|
|
19
33
|
- `RequestTrail::Subscriber` — attach/detach API for notification subscriptions
|
|
20
34
|
- `RequestTrail::Collector` — thread-safe per-request event accumulator
|
|
21
35
|
|
|
22
|
-
[Unreleased]: https://github.com/eclectic-coding/request-trail/compare/v0.
|
|
36
|
+
[Unreleased]: https://github.com/eclectic-coding/request-trail/compare/v0.3.0...HEAD
|
|
37
|
+
[0.3.0]: https://github.com/eclectic-coding/request-trail/releases/tag/v0.3.0
|
|
23
38
|
[0.2.0]: https://github.com/eclectic-coding/request-trail/releases/tag/v0.2.0
|
|
24
39
|
[0.1.0]: https://github.com/eclectic-coding/request-trail/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -36,16 +36,23 @@ gem install request_trail
|
|
|
36
36
|
|
|
37
37
|
### Rails
|
|
38
38
|
|
|
39
|
-
RequestTrail auto-inserts itself via a Railtie. No manual middleware configuration is needed — just add the gem to your `Gemfile` and it will log a summary after every request
|
|
39
|
+
RequestTrail auto-inserts itself via a Railtie. No manual middleware configuration is needed — just add the gem to your `Gemfile` and it will log a summary after every request.
|
|
40
|
+
|
|
41
|
+
When controller tracing is active, output is tiered:
|
|
40
42
|
|
|
41
43
|
```
|
|
42
|
-
[RequestTrail] GET /orders 142ms
|
|
44
|
+
[RequestTrail] GET /orders 142ms
|
|
45
|
+
controller 104ms
|
|
46
|
+
sql 38ms (7 queries)
|
|
47
|
+
cache 2ms (4 hits, 1 miss)
|
|
48
|
+
view 22ms
|
|
43
49
|
```
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
Without controller data (plain Rack apps), a single-line summary is emitted:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
[RequestTrail] GET /orders 142ms | SQL: 7/38.3ms | Cache: 4 hits, 1 miss, 2.0ms
|
|
55
|
+
```
|
|
49
56
|
|
|
50
57
|
### Configuration
|
|
51
58
|
|
|
@@ -78,9 +85,42 @@ run MyApp
|
|
|
78
85
|
|
|
79
86
|
## Development
|
|
80
87
|
|
|
81
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake
|
|
88
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run the full CI suite (audit + lint + tests). You can also run `bin/console` for an interactive prompt.
|
|
89
|
+
|
|
90
|
+
### Running tests
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
bundle exec rake spec # full test suite
|
|
94
|
+
bundle exec rspec spec/path/to/file_spec.rb # single file
|
|
95
|
+
bundle exec rspec spec/path/to/file_spec.rb:42 # single example
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Dummy app
|
|
99
|
+
|
|
100
|
+
A minimal Rails app lives in `spec/dummy` for manual end-to-end testing. It mounts a single `GET /ping` endpoint and logs RequestTrail output to `spec/dummy/log/request_trail.log`.
|
|
82
101
|
|
|
83
|
-
|
|
102
|
+
Start the server:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
bundle exec rackup spec/dummy/config.ru --port 3000
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Then make a request and tail the log:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
curl http://localhost:3000/ping
|
|
112
|
+
tail -f spec/dummy/log/request_trail.log
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
You should see tiered output like:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
[RequestTrail] GET /ping 33ms
|
|
119
|
+
controller 3ms
|
|
120
|
+
sql 0.0ms (0 queries)
|
|
121
|
+
cache 0.0ms (0 hits, 0 misses)
|
|
122
|
+
view 2.8ms
|
|
123
|
+
```
|
|
84
124
|
|
|
85
125
|
[Back to top](#requesttrail)
|
|
86
126
|
|
data/ROADMAP.md
CHANGED
|
@@ -2,18 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
`request_trail` traces a Rails request through every processing layer — middleware, controller, ActiveRecord, cache — and emits a flame-graph-style summary to the log. This roadmap describes the incremental path to a stable 1.0.0.
|
|
4
4
|
|
|
5
|
-
## 0.3.0 — Controller & View Tracing
|
|
6
|
-
|
|
7
|
-
- Subscribe to `process_action.action_controller` and `render_template.action_view`
|
|
8
|
-
- Tiered multi-line breakdown showing time spent in each layer:
|
|
9
|
-
```
|
|
10
|
-
[RequestTrail] GET /orders 142ms
|
|
11
|
-
controller 104ms
|
|
12
|
-
sql 38ms (7 queries)
|
|
13
|
-
cache 2ms (4 hits, 1 miss)
|
|
14
|
-
view 22ms
|
|
15
|
-
```
|
|
16
|
-
|
|
17
5
|
## 0.4.0 — Flame Graph Output
|
|
18
6
|
|
|
19
7
|
- Indented ASCII flame-graph renderer with proportional timing bars
|
|
@@ -34,6 +22,7 @@
|
|
|
34
22
|
- Slow-request mode: only emit summaries above `threshold_ms`
|
|
35
23
|
- Sampling: trace only N% of requests (useful in production)
|
|
36
24
|
- Custom formatter API: `config.formatter = MyFormatter`
|
|
25
|
+
- Rails generator to scaffold the config initializer (`rails generate request_trail:install`)
|
|
37
26
|
|
|
38
27
|
## 0.6.0 — Structured Output & Integrations
|
|
39
28
|
|
|
@@ -5,7 +5,8 @@ module RequestTrail
|
|
|
5
5
|
THREAD_KEY = :request_trail_collector
|
|
6
6
|
|
|
7
7
|
attr_reader :sql_count, :sql_duration_ms,
|
|
8
|
-
:cache_hits, :cache_misses, :cache_writes, :cache_duration_ms
|
|
8
|
+
:cache_hits, :cache_misses, :cache_writes, :cache_duration_ms,
|
|
9
|
+
:action_duration_ms, :view_duration_ms
|
|
9
10
|
|
|
10
11
|
def self.current
|
|
11
12
|
Thread.current[THREAD_KEY]
|
|
@@ -27,6 +28,8 @@ module RequestTrail
|
|
|
27
28
|
@cache_misses = 0
|
|
28
29
|
@cache_writes = 0
|
|
29
30
|
@cache_duration_ms = 0.0
|
|
31
|
+
@action_duration_ms = 0.0
|
|
32
|
+
@view_duration_ms = 0.0
|
|
30
33
|
end
|
|
31
34
|
|
|
32
35
|
def record_sql(duration_ms)
|
|
@@ -44,6 +47,11 @@ module RequestTrail
|
|
|
44
47
|
@cache_duration_ms += duration_ms
|
|
45
48
|
end
|
|
46
49
|
|
|
50
|
+
def record_action(duration_ms:, view_duration_ms:)
|
|
51
|
+
@action_duration_ms = duration_ms
|
|
52
|
+
@view_duration_ms = view_duration_ms
|
|
53
|
+
end
|
|
54
|
+
|
|
47
55
|
def elapsed_ms
|
|
48
56
|
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at
|
|
49
57
|
(elapsed * 1000).round(2)
|
|
@@ -3,12 +3,24 @@
|
|
|
3
3
|
module RequestTrail
|
|
4
4
|
class Formatter
|
|
5
5
|
def format(request, collector)
|
|
6
|
-
header = "[RequestTrail] #{request.request_method} #{request.path}"
|
|
7
|
-
|
|
6
|
+
header = "[RequestTrail] #{request.request_method} #{request.path} #{collector.elapsed_ms.round}ms"
|
|
7
|
+
return tiered_format(header, collector) if collector.action_duration_ms.positive?
|
|
8
|
+
|
|
9
|
+
"#{header} | #{sql_summary(collector)} | #{cache_summary(collector)}"
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
private
|
|
11
13
|
|
|
14
|
+
def tiered_format(header, collector)
|
|
15
|
+
[
|
|
16
|
+
header,
|
|
17
|
+
" controller #{collector.action_duration_ms.round}ms",
|
|
18
|
+
" sql #{collector.sql_duration_ms.round(1)}ms (#{sql_count_label(collector)})",
|
|
19
|
+
" cache #{collector.cache_duration_ms.round(1)}ms (#{cache_detail(collector)})",
|
|
20
|
+
" view #{collector.view_duration_ms.round(1)}ms"
|
|
21
|
+
].join("\n")
|
|
22
|
+
end
|
|
23
|
+
|
|
12
24
|
def sql_summary(collector)
|
|
13
25
|
"SQL: #{collector.sql_count}/#{collector.sql_duration_ms.round(1)}ms"
|
|
14
26
|
end
|
|
@@ -19,5 +31,15 @@ module RequestTrail
|
|
|
19
31
|
duration = collector.cache_duration_ms.round(1)
|
|
20
32
|
"Cache: #{collector.cache_hits} #{hit_label}, #{collector.cache_misses} #{miss_label}, #{duration}ms"
|
|
21
33
|
end
|
|
34
|
+
|
|
35
|
+
def sql_count_label(collector)
|
|
36
|
+
collector.sql_count == 1 ? "1 query" : "#{collector.sql_count} queries"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def cache_detail(collector)
|
|
40
|
+
hit_label = collector.cache_hits == 1 ? "hit" : "hits"
|
|
41
|
+
miss_label = collector.cache_misses == 1 ? "miss" : "misses"
|
|
42
|
+
"#{collector.cache_hits} #{hit_label}, #{collector.cache_misses} #{miss_label}"
|
|
43
|
+
end
|
|
22
44
|
end
|
|
23
45
|
end
|
|
@@ -5,9 +5,10 @@ module RequestTrail
|
|
|
5
5
|
SQL_EVENT = "sql.active_record"
|
|
6
6
|
CACHE_READ_EVENT = "cache_read.active_support"
|
|
7
7
|
CACHE_WRITE_EVENT = "cache_write.active_support"
|
|
8
|
+
ACTION_EVENT = "process_action.action_controller"
|
|
8
9
|
|
|
9
10
|
def self.attach
|
|
10
|
-
@attach ||= [sql_subscription, cache_read_subscription, cache_write_subscription]
|
|
11
|
+
@attach ||= [sql_subscription, cache_read_subscription, cache_write_subscription, action_subscription]
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def self.detach
|
|
@@ -40,5 +41,15 @@ module RequestTrail
|
|
|
40
41
|
Collector.current&.record_cache_write(duration_ms: event.duration)
|
|
41
42
|
end
|
|
42
43
|
end
|
|
44
|
+
|
|
45
|
+
private_class_method def self.action_subscription
|
|
46
|
+
ActiveSupport::Notifications.subscribe(ACTION_EVENT) do |*args|
|
|
47
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
48
|
+
Collector.current&.record_action(
|
|
49
|
+
duration_ms: event.duration,
|
|
50
|
+
view_duration_ms: event.payload.fetch(:view_runtime, 0.0)
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
43
54
|
end
|
|
44
55
|
end
|
|
@@ -9,6 +9,8 @@ module RequestTrail
|
|
|
9
9
|
@cache_misses: Integer
|
|
10
10
|
@cache_writes: Integer
|
|
11
11
|
@cache_duration_ms: Float
|
|
12
|
+
@action_duration_ms: Float
|
|
13
|
+
@view_duration_ms: Float
|
|
12
14
|
|
|
13
15
|
attr_reader sql_count: Integer
|
|
14
16
|
attr_reader sql_duration_ms: Float
|
|
@@ -16,6 +18,8 @@ module RequestTrail
|
|
|
16
18
|
attr_reader cache_misses: Integer
|
|
17
19
|
attr_reader cache_writes: Integer
|
|
18
20
|
attr_reader cache_duration_ms: Float
|
|
21
|
+
attr_reader action_duration_ms: Float
|
|
22
|
+
attr_reader view_duration_ms: Float
|
|
19
23
|
|
|
20
24
|
def self.current: () -> Collector?
|
|
21
25
|
def self.start: () -> Collector
|
|
@@ -25,6 +29,7 @@ module RequestTrail
|
|
|
25
29
|
def record_sql: (Float duration_ms) -> Float
|
|
26
30
|
def record_cache_read: (hit: bool, duration_ms: Float) -> Float
|
|
27
31
|
def record_cache_write: (duration_ms: Float) -> Float
|
|
32
|
+
def record_action: (duration_ms: Float, view_duration_ms: Float) -> Float
|
|
28
33
|
def elapsed_ms: () -> (Float | Integer)
|
|
29
34
|
end
|
|
30
35
|
end
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
module RequestTrail
|
|
2
2
|
class Formatter
|
|
3
3
|
def format: (::Rack::Request request, Collector collector) -> String
|
|
4
|
+
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def tiered_format: (String header, Collector collector) -> String
|
|
8
|
+
def sql_summary: (Collector collector) -> String
|
|
9
|
+
def cache_summary: (Collector collector) -> String
|
|
10
|
+
def sql_count_label: (Collector collector) -> String
|
|
11
|
+
def cache_detail: (Collector collector) -> String
|
|
4
12
|
end
|
|
5
13
|
end
|
|
@@ -3,6 +3,7 @@ module RequestTrail
|
|
|
3
3
|
SQL_EVENT: String
|
|
4
4
|
CACHE_READ_EVENT: String
|
|
5
5
|
CACHE_WRITE_EVENT: String
|
|
6
|
+
ACTION_EVENT: String
|
|
6
7
|
|
|
7
8
|
self.@attach: Array[untyped]?
|
|
8
9
|
|
|
@@ -14,5 +15,6 @@ module RequestTrail
|
|
|
14
15
|
def self.sql_subscription: () -> untyped
|
|
15
16
|
def self.cache_read_subscription: () -> untyped
|
|
16
17
|
def self.cache_write_subscription: () -> untyped
|
|
18
|
+
def self.action_subscription: () -> untyped
|
|
17
19
|
end
|
|
18
20
|
end
|