request_trail 0.1.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 +7 -0
- data/.github/workflows/main.yml +80 -0
- data/.github/workflows/publish.yml +40 -0
- data/CHANGELOG.md +16 -0
- data/CLAUDE.md +57 -0
- data/LICENSE.txt +21 -0
- data/README.md +92 -0
- data/ROADMAP.md +58 -0
- data/Rakefile +13 -0
- data/codecov.yml +14 -0
- data/lib/request_trail/collector.rb +37 -0
- data/lib/request_trail/configuration.rb +26 -0
- data/lib/request_trail/formatter.rb +17 -0
- data/lib/request_trail/middleware.rb +32 -0
- data/lib/request_trail/railtie.rb +12 -0
- data/lib/request_trail/subscriber.rb +21 -0
- data/lib/request_trail/version.rb +5 -0
- data/lib/request_trail.rb +32 -0
- data/sig/request_trail.rbs +3 -0
- metadata +92 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ef1bf3bfcc191f502a9380fbe03bcdba11db1003b6b061db77b93769ac8697ba
|
|
4
|
+
data.tar.gz: 6df870ef886ee153cda6babf4cb75c2d80484c9d42c61fccd926fe5278a42705
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 484a485f9ffd159c6daf55abd72f9dc44204d483b2ed31242e502433224c7adf59b33a211e1fd501d703437caa1c4158775b463f0c3c4cdd7156283078ed3b89
|
|
7
|
+
data.tar.gz: 99d772953da1f97dea448fb3e50bf8889c7a1e29fae0a67805e210d949cddbacad5e039bd7d59821480109c6a62566e73d3575d21a0f61badf3c19e5eec08731
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
env:
|
|
14
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
lint:
|
|
18
|
+
name: Lint
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- name: Check out repository
|
|
23
|
+
uses: actions/checkout@v5
|
|
24
|
+
|
|
25
|
+
- name: Set up Ruby
|
|
26
|
+
uses: ruby/setup-ruby@v1
|
|
27
|
+
with:
|
|
28
|
+
ruby-version: "3.3"
|
|
29
|
+
bundler-cache: true
|
|
30
|
+
|
|
31
|
+
- name: Run RuboCop
|
|
32
|
+
run: bundle exec rubocop
|
|
33
|
+
|
|
34
|
+
security:
|
|
35
|
+
name: Security audit
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
|
|
38
|
+
steps:
|
|
39
|
+
- name: Check out repository
|
|
40
|
+
uses: actions/checkout@v5
|
|
41
|
+
|
|
42
|
+
- name: Set up Ruby
|
|
43
|
+
uses: ruby/setup-ruby@v1
|
|
44
|
+
with:
|
|
45
|
+
ruby-version: "3.3"
|
|
46
|
+
bundler-cache: true
|
|
47
|
+
|
|
48
|
+
- name: Run bundler-audit
|
|
49
|
+
run: bundle exec rake bundle:audit:update bundle:audit:check
|
|
50
|
+
|
|
51
|
+
test:
|
|
52
|
+
name: Ruby ${{ matrix.ruby }}
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
strategy:
|
|
55
|
+
fail-fast: false
|
|
56
|
+
matrix:
|
|
57
|
+
ruby:
|
|
58
|
+
- "3.3"
|
|
59
|
+
- "3.4"
|
|
60
|
+
- "4.0"
|
|
61
|
+
|
|
62
|
+
steps:
|
|
63
|
+
- name: Check out repository
|
|
64
|
+
uses: actions/checkout@v5
|
|
65
|
+
|
|
66
|
+
- name: Set up Ruby
|
|
67
|
+
uses: ruby/setup-ruby@v1
|
|
68
|
+
with:
|
|
69
|
+
ruby-version: ${{ matrix.ruby }}
|
|
70
|
+
bundler-cache: true
|
|
71
|
+
|
|
72
|
+
- name: Run test suite
|
|
73
|
+
run: bundle exec rake spec
|
|
74
|
+
|
|
75
|
+
- name: Upload coverage to Codecov
|
|
76
|
+
uses: codecov/codecov-action@v7
|
|
77
|
+
if: matrix.ruby == '3.4'
|
|
78
|
+
with:
|
|
79
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
80
|
+
files: coverage/coverage.json
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
env:
|
|
9
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
name: Publish to RubyGems
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
permissions:
|
|
16
|
+
contents: write
|
|
17
|
+
id-token: write
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v5
|
|
21
|
+
|
|
22
|
+
- uses: ruby/setup-ruby@v1
|
|
23
|
+
with:
|
|
24
|
+
ruby-version: "3.4"
|
|
25
|
+
bundler-cache: true
|
|
26
|
+
|
|
27
|
+
- name: Run test suite
|
|
28
|
+
run: bundle exec rake
|
|
29
|
+
|
|
30
|
+
- name: Create GitHub Release
|
|
31
|
+
env:
|
|
32
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
33
|
+
run: |
|
|
34
|
+
gh release create "${{ github.ref_name }}" \
|
|
35
|
+
--title "${{ github.ref_name }}" \
|
|
36
|
+
--notes "See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details." \
|
|
37
|
+
--verify-tag
|
|
38
|
+
|
|
39
|
+
- name: Publish to RubyGems
|
|
40
|
+
uses: rubygems/release-gem@v1
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-06-11
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Rack middleware (`RequestTrail::Middleware`) that wraps requests and records wall-clock duration
|
|
8
|
+
- Rails Railtie (`RequestTrail::Railtie`) for automatic middleware insertion
|
|
9
|
+
- ActiveRecord query tracing via `sql.active_record` notifications — records query count and cumulative duration per request
|
|
10
|
+
- Configuration DSL: `RequestTrail.configure { |c| c.enabled = true; c.log_level = :info; c.threshold_ms = 0; c.logger = nil }`
|
|
11
|
+
- Plain-text log formatter producing summaries like `[RequestTrail] GET /orders 142ms | SQL: 7 queries / 38ms`
|
|
12
|
+
- `RequestTrail::Subscriber` — attach/detach API for notification subscriptions
|
|
13
|
+
- `RequestTrail::Collector` — thread-safe per-request event accumulator
|
|
14
|
+
|
|
15
|
+
[Unreleased]: https://github.com/eclectic-coding/request-trail/compare/v0.1.0...HEAD
|
|
16
|
+
[0.1.0]: https://github.com/eclectic-coding/request-trail/releases/tag/v0.1.0
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
`request_trail` is a Ruby gem (module `RequestTrail`) that traces a Rails request through all processing layers and dumps a flame-graph-style summary to the log.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bin/setup # Install dependencies
|
|
13
|
+
bundle exec rake # Full CI suite: bundler-audit + rubocop + rspec (default task)
|
|
14
|
+
bundle exec rake spec # Run tests only
|
|
15
|
+
bundle exec rake rubocop # Lint only
|
|
16
|
+
bundle exec rake rubocop:autocorrect # Auto-fix lint violations
|
|
17
|
+
bundle exec rake bundle:audit:update bundle:audit:check # Security audit
|
|
18
|
+
bundle exec rspec spec/path/to/file_spec.rb # Run a single spec file
|
|
19
|
+
bundle exec rspec spec/path/to/file_spec.rb:42 # Run a single example by line
|
|
20
|
+
bin/console # Interactive prompt with gem loaded
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
lib/
|
|
27
|
+
request_trail.rb # Entry point — RequestTrail module, configure/formatter/reset!
|
|
28
|
+
request_trail/
|
|
29
|
+
version.rb # VERSION constant
|
|
30
|
+
configuration.rb # Config object (enabled, log_level, threshold_ms, logger)
|
|
31
|
+
collector.rb # Per-request event accumulator (Thread.current storage)
|
|
32
|
+
subscriber.rb # ActiveSupport::Notifications subscriber for sql.active_record
|
|
33
|
+
formatter.rb # Plain-text log formatter
|
|
34
|
+
middleware.rb # Rack middleware — wraps request, drives collector lifecycle
|
|
35
|
+
railtie.rb # Rails Railtie — auto-inserts middleware, attaches subscriber
|
|
36
|
+
spec/
|
|
37
|
+
request_trail_spec.rb # Main module spec
|
|
38
|
+
request_trail/ # Per-class specs
|
|
39
|
+
spec_helper.rb # Configures SimpleCov (HTML + JSON), loads rack/activesupport/railties
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Code Style
|
|
43
|
+
|
|
44
|
+
- RuboCop targets Ruby 3.3; `NewCops: enable` means all new cops are active
|
|
45
|
+
- String literals must use **double quotes** (both regular strings and interpolation)
|
|
46
|
+
- `spec/**/*` and `vendor/**/*` are excluded from RuboCop
|
|
47
|
+
- All files use `# frozen_string_literal: true`
|
|
48
|
+
- `Style/Documentation` is disabled — internal classes do not require doc comments
|
|
49
|
+
|
|
50
|
+
## CI
|
|
51
|
+
|
|
52
|
+
The GitHub Actions CI (`.github/workflows/main.yml`) runs three jobs on push/PR:
|
|
53
|
+
- **Lint**: RuboCop on Ruby 3.3
|
|
54
|
+
- **Security**: bundler-audit
|
|
55
|
+
- **Test**: RSpec matrix across Ruby 3.3, 3.4, and 4.0; coverage uploaded to Codecov from the 3.4 run
|
|
56
|
+
|
|
57
|
+
Releases are triggered by `v*` tags via `.github/workflows/publish.yml`, which verifies the tag matches `VERSION`, runs the full suite, builds the gem, and pushes to RubyGems using trusted publishing.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chuck Smith
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# RequestTrail
|
|
2
|
+
|
|
3
|
+
[](https://github.com/eclectic-coding/request-trail/actions/workflows/main.yml)
|
|
4
|
+
[](https://rubygems.org/gems/request_trail)
|
|
5
|
+
[](https://rubygems.org/gems/request_trail)
|
|
6
|
+
[](https://rubygems.org/gems/request_trail)
|
|
7
|
+
[](https://codecov.io/gh/eclectic-coding/request-trail)
|
|
8
|
+
|
|
9
|
+
Middleware that traces a request through all the layers (middleware, controller, ActiveRecord, cache) and dumps a flame-graph-style summary to the log.
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Usage](#usage)
|
|
15
|
+
- [Development](#development)
|
|
16
|
+
- [Contributing](#contributing)
|
|
17
|
+
- [License](#license)
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
Add to your application's Gemfile:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bundle add request_trail
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or install directly:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
gem install request_trail
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
[Back to top](#requesttrail)
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### Rails
|
|
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:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
[RequestTrail] GET /orders 142ms | SQL: 7 queries / 38ms
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Configuration
|
|
46
|
+
|
|
47
|
+
Add an initializer to customize behavior:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
# config/initializers/request_trail.rb
|
|
51
|
+
RequestTrail.configure do |config|
|
|
52
|
+
config.enabled = true # set to false to disable entirely
|
|
53
|
+
config.log_level = :info # Rails logger level (:debug, :info, :warn)
|
|
54
|
+
config.threshold_ms = 200 # only log requests slower than this (0 = log all)
|
|
55
|
+
config.logger = nil # defaults to Rails.logger
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Non-Rails (plain Rack)
|
|
60
|
+
|
|
61
|
+
Insert the middleware manually and attach the subscriber:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
require "request_trail"
|
|
65
|
+
|
|
66
|
+
RequestTrail::Subscriber.attach
|
|
67
|
+
|
|
68
|
+
use RequestTrail::Middleware
|
|
69
|
+
run MyApp
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
[Back to top](#requesttrail)
|
|
73
|
+
|
|
74
|
+
## Development
|
|
75
|
+
|
|
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.
|
|
77
|
+
|
|
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).
|
|
79
|
+
|
|
80
|
+
[Back to top](#requesttrail)
|
|
81
|
+
|
|
82
|
+
## Contributing
|
|
83
|
+
|
|
84
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/eclectic-coding/request-trail.
|
|
85
|
+
|
|
86
|
+
[Back to top](#requesttrail)
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
91
|
+
|
|
92
|
+
[Back to top](#requesttrail)
|
data/ROADMAP.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
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
|
+
|
|
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
|
+
## 0.4.0 — Flame Graph Output
|
|
26
|
+
|
|
27
|
+
- Indented ASCII flame-graph renderer with proportional timing bars
|
|
28
|
+
- Optional ANSI colour with automatic TTY detection
|
|
29
|
+
- Ships as `Request::Trail::Formatters::FlameGraph` alongside the existing plain-text formatter:
|
|
30
|
+
```
|
|
31
|
+
[RequestTrail] GET /orders 142ms ████████████████████████████████████
|
|
32
|
+
middleware 4ms █
|
|
33
|
+
controller 100ms ████████████████████████
|
|
34
|
+
sql 38ms █████████
|
|
35
|
+
cache 2ms
|
|
36
|
+
view 22ms █████
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 0.5.0 — Filtering & Sampling
|
|
40
|
+
|
|
41
|
+
- Path filters: skip tracing for `/assets`, `/health`, or custom regex patterns
|
|
42
|
+
- Slow-request mode: only emit summaries above `threshold_ms`
|
|
43
|
+
- Sampling: trace only N% of requests (useful in production)
|
|
44
|
+
- Custom formatter API: `config.formatter = MyFormatter`
|
|
45
|
+
|
|
46
|
+
## 0.6.0 — Structured Output & Integrations
|
|
47
|
+
|
|
48
|
+
- JSON formatter for log aggregators (Datadog, Splunk, etc.)
|
|
49
|
+
- `config.logger` override for writing to a separate file or custom appender
|
|
50
|
+
- Rails log tags integration
|
|
51
|
+
- Optional Sidekiq adapter for tracing background job execution layers
|
|
52
|
+
|
|
53
|
+
## 1.0.0 — Stable Release
|
|
54
|
+
|
|
55
|
+
- Frozen public API (`Request::Trail.configure`, middleware interface, formatter interface)
|
|
56
|
+
- 100% test coverage
|
|
57
|
+
- Overhead benchmark suite (target: < 1ms added latency per request)
|
|
58
|
+
- Full usage documentation with real-world examples
|
data/Rakefile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rubocop/rake_task"
|
|
5
|
+
require "rspec/core/rake_task"
|
|
6
|
+
require "bundler/audit/task"
|
|
7
|
+
|
|
8
|
+
RuboCop::RakeTask.new
|
|
9
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
10
|
+
Bundler::Audit::Task.new
|
|
11
|
+
|
|
12
|
+
desc "Run bundler-audit, rubocop, and rspec (full CI suite)"
|
|
13
|
+
task default: ["bundle:audit:update", "bundle:audit:check", :rubocop, :spec]
|
data/codecov.yml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
comment:
|
|
2
|
+
layout: "reach, diff, flags, files"
|
|
3
|
+
behavior: default
|
|
4
|
+
# Only post or update the comment if the coverage drops
|
|
5
|
+
require_changes: "coverage_drop"
|
|
6
|
+
|
|
7
|
+
coverage:
|
|
8
|
+
status:
|
|
9
|
+
project:
|
|
10
|
+
default:
|
|
11
|
+
informational: false
|
|
12
|
+
patch:
|
|
13
|
+
default:
|
|
14
|
+
informational: false
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RequestTrail
|
|
4
|
+
class Collector
|
|
5
|
+
THREAD_KEY = :request_trail_collector
|
|
6
|
+
|
|
7
|
+
attr_reader :sql_count, :sql_duration_ms
|
|
8
|
+
|
|
9
|
+
def self.current
|
|
10
|
+
Thread.current[THREAD_KEY]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.start
|
|
14
|
+
Thread.current[THREAD_KEY] = new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.stop
|
|
18
|
+
Thread.current[THREAD_KEY] = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
23
|
+
@sql_count = 0
|
|
24
|
+
@sql_duration_ms = 0.0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def record_sql(duration_ms)
|
|
28
|
+
@sql_count += 1
|
|
29
|
+
@sql_duration_ms += duration_ms
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def elapsed_ms
|
|
33
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at
|
|
34
|
+
(elapsed * 1000).round(2)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
module RequestTrail
|
|
6
|
+
class Configuration
|
|
7
|
+
attr_writer :logger
|
|
8
|
+
attr_accessor :enabled, :log_level, :threshold_ms
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@enabled = true
|
|
12
|
+
@log_level = :info
|
|
13
|
+
@threshold_ms = 0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def logger
|
|
17
|
+
@logger ||= rails_logger || Logger.new($stdout)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def rails_logger
|
|
23
|
+
Rails.logger if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RequestTrail
|
|
4
|
+
class Formatter
|
|
5
|
+
def format(request, collector)
|
|
6
|
+
header = "[RequestTrail] #{request.request_method} #{request.path}"
|
|
7
|
+
"#{header} #{collector.elapsed_ms.round}ms | #{sql_summary(collector)}"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
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"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RequestTrail
|
|
4
|
+
class Middleware
|
|
5
|
+
def initialize(app)
|
|
6
|
+
@app = app
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(env)
|
|
10
|
+
return @app.call(env) unless RequestTrail.configuration.enabled
|
|
11
|
+
|
|
12
|
+
Collector.start
|
|
13
|
+
response = @app.call(env)
|
|
14
|
+
log(env)
|
|
15
|
+
response
|
|
16
|
+
ensure
|
|
17
|
+
Collector.stop
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def log(env)
|
|
23
|
+
collector = Collector.current
|
|
24
|
+
elapsed = collector.elapsed_ms
|
|
25
|
+
return if elapsed < RequestTrail.configuration.threshold_ms
|
|
26
|
+
|
|
27
|
+
request = Rack::Request.new(env)
|
|
28
|
+
message = RequestTrail.formatter.format(request, collector)
|
|
29
|
+
RequestTrail.configuration.logger.send(RequestTrail.configuration.log_level, message)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RequestTrail
|
|
4
|
+
class Subscriber
|
|
5
|
+
SQL_EVENT = "sql.active_record"
|
|
6
|
+
|
|
7
|
+
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
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.detach
|
|
15
|
+
return unless @attach
|
|
16
|
+
|
|
17
|
+
ActiveSupport::Notifications.unsubscribe(@attach)
|
|
18
|
+
@attach = nil
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "request_trail/version"
|
|
4
|
+
require_relative "request_trail/configuration"
|
|
5
|
+
require_relative "request_trail/collector"
|
|
6
|
+
require_relative "request_trail/subscriber"
|
|
7
|
+
require_relative "request_trail/formatter"
|
|
8
|
+
require_relative "request_trail/middleware"
|
|
9
|
+
require_relative "request_trail/railtie" if defined?(Rails::Railtie)
|
|
10
|
+
|
|
11
|
+
module RequestTrail
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def configure
|
|
16
|
+
yield configuration
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def configuration
|
|
20
|
+
@configuration ||= Configuration.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def formatter
|
|
24
|
+
@formatter ||= Formatter.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reset!
|
|
28
|
+
@configuration = nil
|
|
29
|
+
@formatter = nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: request_trail
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Chuck Smith
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '6.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rack
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
description: Middleware that traces a request through middleware, controller, ActiveRecord,
|
|
41
|
+
and cache layers, then dumps a flame-graph-style summary to the log.
|
|
42
|
+
email:
|
|
43
|
+
- eclectic-coding@users.noreply.github.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- ".github/workflows/main.yml"
|
|
49
|
+
- ".github/workflows/publish.yml"
|
|
50
|
+
- CHANGELOG.md
|
|
51
|
+
- CLAUDE.md
|
|
52
|
+
- LICENSE.txt
|
|
53
|
+
- README.md
|
|
54
|
+
- ROADMAP.md
|
|
55
|
+
- Rakefile
|
|
56
|
+
- codecov.yml
|
|
57
|
+
- lib/request_trail.rb
|
|
58
|
+
- lib/request_trail/collector.rb
|
|
59
|
+
- lib/request_trail/configuration.rb
|
|
60
|
+
- lib/request_trail/formatter.rb
|
|
61
|
+
- lib/request_trail/middleware.rb
|
|
62
|
+
- lib/request_trail/railtie.rb
|
|
63
|
+
- lib/request_trail/subscriber.rb
|
|
64
|
+
- lib/request_trail/version.rb
|
|
65
|
+
- sig/request_trail.rbs
|
|
66
|
+
homepage: https://github.com/eclectic-coding/request-trail
|
|
67
|
+
licenses:
|
|
68
|
+
- MIT
|
|
69
|
+
metadata:
|
|
70
|
+
homepage_uri: https://github.com/eclectic-coding/request-trail
|
|
71
|
+
source_code_uri: https://github.com/eclectic-coding/request-trail
|
|
72
|
+
changelog_uri: https://github.com/eclectic-coding/request-trail/blob/main/CHANGELOG.md
|
|
73
|
+
rubygems_mfa_required: 'true'
|
|
74
|
+
rdoc_options: []
|
|
75
|
+
require_paths:
|
|
76
|
+
- lib
|
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 3.3.0
|
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - ">="
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '0'
|
|
87
|
+
requirements: []
|
|
88
|
+
rubygems_version: 3.6.9
|
|
89
|
+
specification_version: 4
|
|
90
|
+
summary: Traces a Rails request through all layers and dumps a flame-graph summary
|
|
91
|
+
to the log.
|
|
92
|
+
test_files: []
|