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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef1bf3bfcc191f502a9380fbe03bcdba11db1003b6b061db77b93769ac8697ba
4
- data.tar.gz: 6df870ef886ee153cda6babf4cb75c2d80484c9d42c61fccd926fe5278a42705
3
+ metadata.gz: 954b1805423f92d59582e41e86d55ac489577ef5c76d20701c0fbcd76956f2a8
4
+ data.tar.gz: 8464e352949c6eb14636ead83e5f0a556b8eb7dcabfd35186f35b3749ae8ac0a
5
5
  SHA512:
6
- metadata.gz: 484a485f9ffd159c6daf55abd72f9dc44204d483b2ed31242e502433224c7adf59b33a211e1fd501d703437caa1c4158775b463f0c3c4cdd7156283078ed3b89
7
- data.tar.gz: 99d772953da1f97dea448fb3e50bf8889c7a1e29fae0a67805e210d949cddbacad5e039bd7d59821480109c6a62566e73d3575d21a0f61badf3c19e5eec08731
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.1.0...HEAD
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 queries / 38ms
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 spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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
- "#{header} #{collector.elapsed_ms.round}ms | #{sql_summary(collector)}"
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
- label = collector.sql_count == 1 ? "query" : "queries"
14
- "SQL: #{collector.sql_count} #{label} / #{collector.sql_duration_ms.round(1)}ms"
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 = "sql.active_record"
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 ||= ActiveSupport::Notifications.subscribe(SQL_EVENT) do |*args|
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(@attach)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RequestTrail
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  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,11 @@
1
+ module RequestTrail
2
+ class Configuration
3
+ attr_writer logger: ::Logger
4
+ attr_accessor enabled: bool
5
+ attr_accessor log_level: Symbol
6
+ attr_accessor threshold_ms: Numeric
7
+
8
+ def initialize: () -> void
9
+ def logger: () -> ::Logger
10
+ end
11
+ 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,6 @@
1
+ module RequestTrail
2
+ class Middleware
3
+ def initialize: (untyped app) -> void
4
+ def call: (Hash[String, untyped] env) -> untyped
5
+ end
6
+ 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.1.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