request_trail 0.1.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 +24 -1
- data/README.md +49 -4
- data/ROADMAP.md +1 -20
- data/lib/request_trail/collector.rb +24 -1
- data/lib/request_trail/formatter.rb +32 -4
- data/lib/request_trail/subscriber.rb +40 -6
- data/lib/request_trail/version.rb +1 -1
- data/sig/request_trail/collector.rbs +35 -0
- data/sig/request_trail/configuration.rbs +11 -0
- data/sig/request_trail/formatter.rbs +13 -0
- data/sig/request_trail/middleware.rbs +6 -0
- data/sig/request_trail/subscriber.rbs +20 -0
- metadata +6 -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,26 @@
|
|
|
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
|
+
|
|
17
|
+
## [0.2.0] - 2026-06-11
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Cache tracing via `cache_read.active_support` and `cache_write.active_support` notifications — records hit/miss/write counts and cumulative duration per request
|
|
22
|
+
- Summary line now includes a cache segment: `[RequestTrail] GET /orders 142ms | SQL: 7/38.3ms | Cache: 4 hits, 1 miss, 2.0ms`
|
|
23
|
+
|
|
3
24
|
## [0.1.0] - 2026-06-11
|
|
4
25
|
|
|
5
26
|
### Added
|
|
@@ -12,5 +33,7 @@
|
|
|
12
33
|
- `RequestTrail::Subscriber` — attach/detach API for notification subscriptions
|
|
13
34
|
- `RequestTrail::Collector` — thread-safe per-request event accumulator
|
|
14
35
|
|
|
15
|
-
[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
|
|
38
|
+
[0.2.0]: https://github.com/eclectic-coding/request-trail/releases/tag/v0.2.0
|
|
16
39
|
[0.1.0]: https://github.com/eclectic-coding/request-trail/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -36,10 +36,22 @@ 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:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
[RequestTrail] GET /orders 142ms
|
|
45
|
+
controller 104ms
|
|
46
|
+
sql 38ms (7 queries)
|
|
47
|
+
cache 2ms (4 hits, 1 miss)
|
|
48
|
+
view 22ms
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Without controller data (plain Rack apps), a single-line summary is emitted:
|
|
40
52
|
|
|
41
53
|
```
|
|
42
|
-
[RequestTrail] GET /orders 142ms | SQL: 7
|
|
54
|
+
[RequestTrail] GET /orders 142ms | SQL: 7/38.3ms | Cache: 4 hits, 1 miss, 2.0ms
|
|
43
55
|
```
|
|
44
56
|
|
|
45
57
|
### Configuration
|
|
@@ -73,9 +85,42 @@ run MyApp
|
|
|
73
85
|
|
|
74
86
|
## Development
|
|
75
87
|
|
|
76
|
-
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`.
|
|
77
101
|
|
|
78
|
-
|
|
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
|
+
```
|
|
79
124
|
|
|
80
125
|
[Back to top](#requesttrail)
|
|
81
126
|
|
data/ROADMAP.md
CHANGED
|
@@ -2,26 +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.2.0 — Cache Tracing
|
|
6
|
-
|
|
7
|
-
- Subscribe to `cache_read`, `cache_write`, `cache_fetch_hit`, and `cache_delete` notifications
|
|
8
|
-
- Add cache hit/miss/write counts and cumulative time to the summary line:
|
|
9
|
-
```
|
|
10
|
-
[RequestTrail] GET /orders 142ms | SQL: 7/38ms | Cache: 4 hits, 1 miss, 2ms
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## 0.3.0 — Controller & View Tracing
|
|
14
|
-
|
|
15
|
-
- Subscribe to `process_action.action_controller` and `render_template.action_view`
|
|
16
|
-
- Tiered multi-line breakdown showing time spent in each layer:
|
|
17
|
-
```
|
|
18
|
-
[RequestTrail] GET /orders 142ms
|
|
19
|
-
controller 104ms
|
|
20
|
-
sql 38ms (7 queries)
|
|
21
|
-
cache 2ms (4 hits, 1 miss)
|
|
22
|
-
view 22ms
|
|
23
|
-
```
|
|
24
|
-
|
|
25
5
|
## 0.4.0 — Flame Graph Output
|
|
26
6
|
|
|
27
7
|
- Indented ASCII flame-graph renderer with proportional timing bars
|
|
@@ -42,6 +22,7 @@
|
|
|
42
22
|
- Slow-request mode: only emit summaries above `threshold_ms`
|
|
43
23
|
- Sampling: trace only N% of requests (useful in production)
|
|
44
24
|
- Custom formatter API: `config.formatter = MyFormatter`
|
|
25
|
+
- Rails generator to scaffold the config initializer (`rails generate request_trail:install`)
|
|
45
26
|
|
|
46
27
|
## 0.6.0 — Structured Output & Integrations
|
|
47
28
|
|
|
@@ -4,7 +4,9 @@ module RequestTrail
|
|
|
4
4
|
class Collector
|
|
5
5
|
THREAD_KEY = :request_trail_collector
|
|
6
6
|
|
|
7
|
-
attr_reader :sql_count, :sql_duration_ms
|
|
7
|
+
attr_reader :sql_count, :sql_duration_ms,
|
|
8
|
+
:cache_hits, :cache_misses, :cache_writes, :cache_duration_ms,
|
|
9
|
+
:action_duration_ms, :view_duration_ms
|
|
8
10
|
|
|
9
11
|
def self.current
|
|
10
12
|
Thread.current[THREAD_KEY]
|
|
@@ -22,6 +24,12 @@ module RequestTrail
|
|
|
22
24
|
@started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
23
25
|
@sql_count = 0
|
|
24
26
|
@sql_duration_ms = 0.0
|
|
27
|
+
@cache_hits = 0
|
|
28
|
+
@cache_misses = 0
|
|
29
|
+
@cache_writes = 0
|
|
30
|
+
@cache_duration_ms = 0.0
|
|
31
|
+
@action_duration_ms = 0.0
|
|
32
|
+
@view_duration_ms = 0.0
|
|
25
33
|
end
|
|
26
34
|
|
|
27
35
|
def record_sql(duration_ms)
|
|
@@ -29,6 +37,21 @@ module RequestTrail
|
|
|
29
37
|
@sql_duration_ms += duration_ms
|
|
30
38
|
end
|
|
31
39
|
|
|
40
|
+
def record_cache_read(hit:, duration_ms:)
|
|
41
|
+
hit ? @cache_hits += 1 : @cache_misses += 1
|
|
42
|
+
@cache_duration_ms += duration_ms
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def record_cache_write(duration_ms:)
|
|
46
|
+
@cache_writes += 1
|
|
47
|
+
@cache_duration_ms += duration_ms
|
|
48
|
+
end
|
|
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
|
+
|
|
32
55
|
def elapsed_ms
|
|
33
56
|
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at
|
|
34
57
|
(elapsed * 1000).round(2)
|
|
@@ -3,15 +3,43 @@
|
|
|
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
|
-
|
|
14
|
-
|
|
25
|
+
"SQL: #{collector.sql_count}/#{collector.sql_duration_ms.round(1)}ms"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def cache_summary(collector)
|
|
29
|
+
hit_label = collector.cache_hits == 1 ? "hit" : "hits"
|
|
30
|
+
miss_label = collector.cache_misses == 1 ? "miss" : "misses"
|
|
31
|
+
duration = collector.cache_duration_ms.round(1)
|
|
32
|
+
"Cache: #{collector.cache_hits} #{hit_label}, #{collector.cache_misses} #{miss_label}, #{duration}ms"
|
|
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}"
|
|
15
43
|
end
|
|
16
44
|
end
|
|
17
45
|
end
|
|
@@ -2,20 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
module RequestTrail
|
|
4
4
|
class Subscriber
|
|
5
|
-
SQL_EVENT
|
|
5
|
+
SQL_EVENT = "sql.active_record"
|
|
6
|
+
CACHE_READ_EVENT = "cache_read.active_support"
|
|
7
|
+
CACHE_WRITE_EVENT = "cache_write.active_support"
|
|
8
|
+
ACTION_EVENT = "process_action.action_controller"
|
|
6
9
|
|
|
7
10
|
def self.attach
|
|
8
|
-
@attach ||=
|
|
9
|
-
event = ActiveSupport::Notifications::Event.new(*args)
|
|
10
|
-
Collector.current&.record_sql(event.duration)
|
|
11
|
-
end
|
|
11
|
+
@attach ||= [sql_subscription, cache_read_subscription, cache_write_subscription, action_subscription]
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def self.detach
|
|
15
15
|
return unless @attach
|
|
16
16
|
|
|
17
|
-
ActiveSupport::Notifications.unsubscribe(
|
|
17
|
+
@attach&.each { |sub| ActiveSupport::Notifications.unsubscribe(sub) }
|
|
18
18
|
@attach = nil
|
|
19
19
|
end
|
|
20
|
+
|
|
21
|
+
private_class_method def self.sql_subscription
|
|
22
|
+
ActiveSupport::Notifications.subscribe(SQL_EVENT) do |*args|
|
|
23
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
24
|
+
Collector.current&.record_sql(event.duration)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private_class_method def self.cache_read_subscription
|
|
29
|
+
ActiveSupport::Notifications.subscribe(CACHE_READ_EVENT) do |*args|
|
|
30
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
31
|
+
Collector.current&.record_cache_read(
|
|
32
|
+
hit: event.payload.fetch(:hit, false),
|
|
33
|
+
duration_ms: event.duration
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private_class_method def self.cache_write_subscription
|
|
39
|
+
ActiveSupport::Notifications.subscribe(CACHE_WRITE_EVENT) do |*args|
|
|
40
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
41
|
+
Collector.current&.record_cache_write(duration_ms: event.duration)
|
|
42
|
+
end
|
|
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
|
|
20
54
|
end
|
|
21
55
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module RequestTrail
|
|
2
|
+
class Collector
|
|
3
|
+
THREAD_KEY: Symbol
|
|
4
|
+
|
|
5
|
+
@started_at: Float
|
|
6
|
+
@sql_count: Integer
|
|
7
|
+
@sql_duration_ms: Float
|
|
8
|
+
@cache_hits: Integer
|
|
9
|
+
@cache_misses: Integer
|
|
10
|
+
@cache_writes: Integer
|
|
11
|
+
@cache_duration_ms: Float
|
|
12
|
+
@action_duration_ms: Float
|
|
13
|
+
@view_duration_ms: Float
|
|
14
|
+
|
|
15
|
+
attr_reader sql_count: Integer
|
|
16
|
+
attr_reader sql_duration_ms: Float
|
|
17
|
+
attr_reader cache_hits: Integer
|
|
18
|
+
attr_reader cache_misses: Integer
|
|
19
|
+
attr_reader cache_writes: Integer
|
|
20
|
+
attr_reader cache_duration_ms: Float
|
|
21
|
+
attr_reader action_duration_ms: Float
|
|
22
|
+
attr_reader view_duration_ms: Float
|
|
23
|
+
|
|
24
|
+
def self.current: () -> Collector?
|
|
25
|
+
def self.start: () -> Collector
|
|
26
|
+
def self.stop: () -> nil
|
|
27
|
+
|
|
28
|
+
def initialize: () -> void
|
|
29
|
+
def record_sql: (Float duration_ms) -> Float
|
|
30
|
+
def record_cache_read: (hit: bool, duration_ms: Float) -> Float
|
|
31
|
+
def record_cache_write: (duration_ms: Float) -> Float
|
|
32
|
+
def record_action: (duration_ms: Float, view_duration_ms: Float) -> Float
|
|
33
|
+
def elapsed_ms: () -> (Float | Integer)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module RequestTrail
|
|
2
|
+
class Formatter
|
|
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
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module RequestTrail
|
|
2
|
+
class Subscriber
|
|
3
|
+
SQL_EVENT: String
|
|
4
|
+
CACHE_READ_EVENT: String
|
|
5
|
+
CACHE_WRITE_EVENT: String
|
|
6
|
+
ACTION_EVENT: String
|
|
7
|
+
|
|
8
|
+
self.@attach: Array[untyped]?
|
|
9
|
+
|
|
10
|
+
def self.attach: () -> Array[untyped]
|
|
11
|
+
def self.detach: () -> nil
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def self.sql_subscription: () -> untyped
|
|
16
|
+
def self.cache_read_subscription: () -> untyped
|
|
17
|
+
def self.cache_write_subscription: () -> untyped
|
|
18
|
+
def self.action_subscription: () -> untyped
|
|
19
|
+
end
|
|
20
|
+
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.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -63,6 +63,11 @@ files:
|
|
|
63
63
|
- lib/request_trail/subscriber.rb
|
|
64
64
|
- lib/request_trail/version.rb
|
|
65
65
|
- sig/request_trail.rbs
|
|
66
|
+
- sig/request_trail/collector.rbs
|
|
67
|
+
- sig/request_trail/configuration.rbs
|
|
68
|
+
- sig/request_trail/formatter.rbs
|
|
69
|
+
- sig/request_trail/middleware.rbs
|
|
70
|
+
- sig/request_trail/subscriber.rbs
|
|
66
71
|
homepage: https://github.com/eclectic-coding/request-trail
|
|
67
72
|
licenses:
|
|
68
73
|
- MIT
|