julewire-rails 1.0.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/CHANGELOG.md +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +94 -0
- data/docs/advanced-configuration.md +17 -0
- data/docs/capture-and-filtering.md +102 -0
- data/docs/configuration.md +83 -0
- data/docs/development.md +21 -0
- data/docs/events-and-errors.md +46 -0
- data/docs/lifecycle.md +24 -0
- data/docs/request-logging.md +49 -0
- data/julewire-rails.gemspec +44 -0
- data/lib/generators/julewire/install_generator.rb +15 -0
- data/lib/generators/julewire/templates/julewire.rb +16 -0
- data/lib/julewire/rails/configuration.rb +74 -0
- data/lib/julewire/rails/context_body_proxy.rb +54 -0
- data/lib/julewire/rails/debug_exception_log_silencer.rb +53 -0
- data/lib/julewire/rails/doctor_app.rb +233 -0
- data/lib/julewire/rails/exception_severity.rb +27 -0
- data/lib/julewire/rails/lifecycle_hooks.rb +76 -0
- data/lib/julewire/rails/log_subscriber_silencer.rb +52 -0
- data/lib/julewire/rails/logger.rb +185 -0
- data/lib/julewire/rails/logger_outputs.rb +36 -0
- data/lib/julewire/rails/output_requirement.rb +38 -0
- data/lib/julewire/rails/parameter_filter_plan.rb +100 -0
- data/lib/julewire/rails/parameter_filter_processor.rb +117 -0
- data/lib/julewire/rails/railtie.rb +84 -0
- data/lib/julewire/rails/request_attributes.rb +126 -0
- data/lib/julewire/rails/request_completion.rb +120 -0
- data/lib/julewire/rails/request_context.rb +91 -0
- data/lib/julewire/rails/request_error_ownership.rb +63 -0
- data/lib/julewire/rails/request_fields.rb +61 -0
- data/lib/julewire/rails/request_lifecycle.rb +109 -0
- data/lib/julewire/rails/request_middleware.rb +130 -0
- data/lib/julewire/rails/request_summary_timeout_scheduler.rb +38 -0
- data/lib/julewire/rails/structured_event_record.rb +128 -0
- data/lib/julewire/rails/subscribers/controller_response.rb +118 -0
- data/lib/julewire/rails/subscribers/error.rb +86 -0
- data/lib/julewire/rails/subscribers/event.rb +118 -0
- data/lib/julewire/rails/subscribers/rendered_exception.rb +141 -0
- data/lib/julewire/rails/suppression.rb +29 -0
- data/lib/julewire/rails/version.rb +7 -0
- data/lib/julewire/rails.rb +37 -0
- data/lib/julewire-rails.rb +3 -0
- metadata +201 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 257bcc53a4cbe56820965e9fce773bda793b5c6f58e6bb336f8458809fe0a946
|
|
4
|
+
data.tar.gz: 3573b4e999ac109ec1f4ab4f80141ad0a27f12d87849468074161e94c96c8d1f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 654930df34fb73f8f2a8b599e1ccd966d06a0d73db00af1b4ac4e6edbaf3eca18bfa8c47adf6f314b73d20eceb76e9f6aaec839fa58df65744dab305966a3e4f
|
|
7
|
+
data.tar.gz: 16db8923eee46a3b948a5878ec2e1e4577562a42536a1b54e8a93c12b2944a50a207357927c6ac7226b873ee7f6ef8a8ae65c572e8e3f5cdd1d4cbc188338505
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexander Grebennik
|
|
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,94 @@
|
|
|
1
|
+
# Julewire Rails
|
|
2
|
+
|
|
3
|
+
Rails integration for Julewire.
|
|
4
|
+
|
|
5
|
+
It installs a Rails-compatible logger, wraps Rack requests in Julewire
|
|
6
|
+
execution scopes, subscribes to Rails structured events, and records Rails error
|
|
7
|
+
reports. It targets Rails 8.1 and newer.
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "julewire-rails"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Configure a Julewire output or custom destination:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
Julewire.configure do |config|
|
|
19
|
+
config.destinations.use(:default, output: $stdout)
|
|
20
|
+
config.level = :info
|
|
21
|
+
|
|
22
|
+
config.processors.prepend(
|
|
23
|
+
:rails_parameter_filter,
|
|
24
|
+
Rails.application.config.filter_parameters
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or generate the initializer:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
bin/rails generate julewire:install
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Use normal Rails APIs:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
Rails.logger.info("booted")
|
|
39
|
+
Rails.logger.warn(message: "retrying", event: "payment.retry", payment_id: 123)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Default behavior:
|
|
43
|
+
|
|
44
|
+
- `Rails.logger.*` calls become Julewire point records.
|
|
45
|
+
- Each request gets one `request.completed` summary.
|
|
46
|
+
- Rails 8.1 structured events become machine point records.
|
|
47
|
+
- `Rails.error` reports become `rails.error` point records.
|
|
48
|
+
- Automatic request exceptions are owned by the request summary.
|
|
49
|
+
- Rails default text subscribers are silenced when Julewire installs the Rails
|
|
50
|
+
logger.
|
|
51
|
+
|
|
52
|
+
Request context stays small: request id, method, path, and remote IP. Rails
|
|
53
|
+
routing, status, timings, completion, and optional capture fields live under
|
|
54
|
+
`attributes.rails`.
|
|
55
|
+
|
|
56
|
+
Rails filters structured event payloads before Julewire receives them.
|
|
57
|
+
Whole-record filtering for `Rails.logger` payloads and optional captured fields
|
|
58
|
+
is a Julewire processor policy; use the registered `:rails_parameter_filter`
|
|
59
|
+
processor when Rails' `filter_parameters` should apply to the full Julewire
|
|
60
|
+
record.
|
|
61
|
+
|
|
62
|
+
## Docs
|
|
63
|
+
|
|
64
|
+
- [Configuration](docs/configuration.md)
|
|
65
|
+
- [Advanced Configuration](docs/advanced-configuration.md)
|
|
66
|
+
- [Request Logging](docs/request-logging.md)
|
|
67
|
+
- [Events and Errors](docs/events-and-errors.md)
|
|
68
|
+
- [Capture and Filtering](docs/capture-and-filtering.md)
|
|
69
|
+
- [Lifecycle](docs/lifecycle.md)
|
|
70
|
+
- [Development](docs/development.md)
|
|
71
|
+
|
|
72
|
+
## Local Doctor
|
|
73
|
+
|
|
74
|
+
Mount the doctor app in development when you want a tiny health and tail view:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
tail = Julewire.dev!(tail: { capacity: 500 })
|
|
78
|
+
mount Julewire::Rails::DoctorApp.new(tail: tail) => "/julewire"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The mounted app serves `/doctor`, `/doctor.json`, `/tail`, `/tail.json`, and a
|
|
82
|
+
reconnecting `/tail/events` stream for the live tail page.
|
|
83
|
+
|
|
84
|
+
The mounted app exposes health, tail buffers, and request paths. Gate it with
|
|
85
|
+
environment, auth, IP rules, or an internal network boundary.
|
|
86
|
+
|
|
87
|
+
Exclude the mount path from request summaries so the live tail does not capture
|
|
88
|
+
its own polling requests:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
Julewire::Rails.configure do |config|
|
|
92
|
+
config.request_exclude_prefixes = ["/julewire"]
|
|
93
|
+
end
|
|
94
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Advanced Configuration
|
|
2
|
+
|
|
3
|
+
These options are rarely needed for normal Rails apps.
|
|
4
|
+
|
|
5
|
+
| Option | Default | Purpose |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| `logger_name` | `"Rails"` | Logger name on Rails logger records. |
|
|
8
|
+
| `lifecycle_hooks` | `true` | Install at-exit and Rails fork hooks. |
|
|
9
|
+
| `log_rescued_responses` | `:auto` | Control Rails raw rescued-response exception text. |
|
|
10
|
+
| `reported_exception_logs` | `:auto` | Control raw `ActionDispatch::DebugExceptions` report text. |
|
|
11
|
+
| `require_output` | `:warn` | Warn/raise when Julewire owns `Rails.logger` but has no output. |
|
|
12
|
+
| `shutdown_timeout` | `1` | Timeout used by lifecycle flush/close hooks. |
|
|
13
|
+
| `summary_event` | `"request.completed"` | Event name for request summaries. |
|
|
14
|
+
|
|
15
|
+
`log_rescued_responses` and `reported_exception_logs` should usually stay on
|
|
16
|
+
`:auto`. That lets Julewire suppress duplicate framework text when it owns the
|
|
17
|
+
Rails logger while keeping Rails' own behavior when it does not.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Capture and Filtering
|
|
2
|
+
|
|
3
|
+
Header and body capture is off by default. Captured fields are summary
|
|
4
|
+
enrichment fields under `attributes.rails`.
|
|
5
|
+
|
|
6
|
+
Response body capture reads the buffered `ActionDispatch::Response` body from
|
|
7
|
+
`process_action.action_controller`. It does not consume or replace the Rack
|
|
8
|
+
response body. File and stream-like responses are skipped. Streaming response
|
|
9
|
+
bodies are not captured. Mounted Rack apps still get request summaries, but
|
|
10
|
+
header and body capture requires a Rails controller `process_action` event.
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
Julewire::Rails.configure do |config|
|
|
14
|
+
config.response_capture.body = true
|
|
15
|
+
config.response_capture.body_bytes = nil
|
|
16
|
+
config.response_capture.body_content_types = %w[application/json]
|
|
17
|
+
end
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Use `body = :json` to parse captured JSON into `request_body_json` or
|
|
21
|
+
`response_body_json` instead of storing the raw body string. Truncated bodies
|
|
22
|
+
are not parsed; invalid JSON records `*_body_parse_error`.
|
|
23
|
+
When `body_bytes` is `nil`, `:json` mode parses the full captured body before
|
|
24
|
+
core serialization limits apply.
|
|
25
|
+
|
|
26
|
+
Captured response summaries include:
|
|
27
|
+
|
|
28
|
+
- `response_body`
|
|
29
|
+
- `response_body_json`
|
|
30
|
+
- `response_body_parse_error`
|
|
31
|
+
- `response_body_bytes`
|
|
32
|
+
- `response_body_truncated`
|
|
33
|
+
|
|
34
|
+
`*_body_bytes` is the content length when the framework provides it; otherwise
|
|
35
|
+
it is the number of bytes observed while capturing.
|
|
36
|
+
|
|
37
|
+
Request and response headers use `false`, `true`, or an explicit header list:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
Julewire::Rails.configure do |config|
|
|
41
|
+
config.request_capture.headers = true
|
|
42
|
+
config.response_capture.headers = %w[content-type x-request-id]
|
|
43
|
+
config.request_capture.body = true
|
|
44
|
+
config.request_capture.body_bytes = 65_536
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
When header capture is `true`, Julewire omits common sensitive headers such as
|
|
49
|
+
`authorization`, `cookie`, `set-cookie`, `proxy-authorization`, and
|
|
50
|
+
`x-api-key`. Use an explicit header list if the application deliberately wants
|
|
51
|
+
one of those fields.
|
|
52
|
+
|
|
53
|
+
Captured request summaries can include:
|
|
54
|
+
|
|
55
|
+
- `request_headers`
|
|
56
|
+
- `response_headers`
|
|
57
|
+
- `request_body`
|
|
58
|
+
- `request_body_json`
|
|
59
|
+
- `request_body_parse_error`
|
|
60
|
+
- `request_body_bytes`
|
|
61
|
+
- `request_body_truncated`
|
|
62
|
+
|
|
63
|
+
Use processors before enabling broad header/body capture in an application with
|
|
64
|
+
sensitive data.
|
|
65
|
+
|
|
66
|
+
Body capture is gated by media type. The default captures `application/json`
|
|
67
|
+
and `application/*+json` vendor media types. `true` means every non-file,
|
|
68
|
+
buffered candidate. Binary media types such as `application/octet-stream`,
|
|
69
|
+
images, audio, video, fonts, zip, gzip, and PDF are skipped even when body
|
|
70
|
+
content types are set to `true`.
|
|
71
|
+
|
|
72
|
+
Rails filters hash-based `Rails.event` payloads before subscribers receive
|
|
73
|
+
them. Object event payloads are different: Rails passes the object through, and
|
|
74
|
+
subscribers that serialize it must filter the serialized fields. Julewire Rails
|
|
75
|
+
filters serialized object-event hashes with
|
|
76
|
+
`Rails.application.filter_parameters` by default.
|
|
77
|
+
|
|
78
|
+
For whole-record filtering, Rails' parameter filter can be installed as a
|
|
79
|
+
Julewire processor:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
Julewire.configure do |config|
|
|
83
|
+
config.processors.prepend(
|
|
84
|
+
:rails_parameter_filter,
|
|
85
|
+
Rails.application.config.filter_parameters
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Record container fields such as `payload`, `attributes`, and `context` keep
|
|
91
|
+
their Julewire shape even when a filter matches the section name.
|
|
92
|
+
|
|
93
|
+
Filtering layers:
|
|
94
|
+
|
|
95
|
+
| Layer | Scope |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| Rails event filtering | Serialized object event payloads before the Julewire record is built. |
|
|
98
|
+
| `ParameterFilterProcessor` | Whole Julewire records using Rails parameter filter rules. |
|
|
99
|
+
| Custom processors | Whole Julewire records using application policy. |
|
|
100
|
+
|
|
101
|
+
If more than one whole-record processor is installed, normal Julewire processor
|
|
102
|
+
order decides precedence.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
Rails config lives on `Julewire::Rails.config` and on
|
|
4
|
+
`config.julewire_rails` in a Rails application.
|
|
5
|
+
|
|
6
|
+
When Julewire owns `Rails.logger`, Rails logger calls are gated by
|
|
7
|
+
`Rails.logger.level`. They bypass core `Julewire.config.level` because the Rails
|
|
8
|
+
logger already applied its level check. Direct `Julewire.emit` and severity
|
|
9
|
+
helper calls still use core `Julewire.config.level`.
|
|
10
|
+
|
|
11
|
+
## Default Path
|
|
12
|
+
|
|
13
|
+
These defaults are intended for a normal Rails app:
|
|
14
|
+
|
|
15
|
+
| Option | Default | Purpose |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `logger` | `true` | Install Julewire as `Rails.logger`. |
|
|
18
|
+
| `request_middleware` | `true` | Wrap Rack requests in Julewire executions. |
|
|
19
|
+
| `request_summary` | `true` | Emit one `request.completed` summary per request. |
|
|
20
|
+
| `request_context` | `true` | Add request id, method, path, and remote IP to context. |
|
|
21
|
+
| `structured_events` | `true` | Subscribe to selected Rails structured events. |
|
|
22
|
+
| `error_reports` | `true` | Subscribe to `Rails.error` reports. |
|
|
23
|
+
| `source` | `"rails"` | Source value on Rails records. |
|
|
24
|
+
| `silence_log_subscribers` | `:auto` | Silence Rails default text subscribers when Julewire owns `Rails.logger`. |
|
|
25
|
+
| `replace_rack_logger` | `true` | Replace `Rails::Rack::Logger` with Julewire request middleware. |
|
|
26
|
+
|
|
27
|
+
## Common Knobs
|
|
28
|
+
|
|
29
|
+
| Option | Default | Purpose |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| `carry_request_headers` | `false` | Explicit request-header list copied into `Julewire.carry`. |
|
|
32
|
+
| `request_exclude_prefixes` | `[]` | Request path prefixes that bypass Julewire request context and summaries. |
|
|
33
|
+
| `request_summary_timeout` | `30` | Seconds before warning that body close/completion is late. It does not finish the summary. |
|
|
34
|
+
| `request_capture.headers` | `false` | Add selected request headers to `attributes.rails` on summaries. |
|
|
35
|
+
| `request_capture.body` | `false` | Add request body fields to `attributes.rails` on summaries. |
|
|
36
|
+
| `response_capture.headers` | `false` | Add selected response headers to `attributes.rails` on summaries. |
|
|
37
|
+
| `response_capture.body` | `false` | Add response body fields to `attributes.rails` on summaries. |
|
|
38
|
+
| `filter_event_payloads` | `true` | Filter serialized object-event hashes with Rails parameter filters. |
|
|
39
|
+
| `rendered_exceptions` | `false` | Emit diagnostic rendered-exception point records in addition to summaries. |
|
|
40
|
+
|
|
41
|
+
Broad header capture (`*.headers = true`) skips common sensitive headers.
|
|
42
|
+
Explicit header lists are authoritative.
|
|
43
|
+
|
|
44
|
+
`structured_event_prefixes = nil` accepts all structured-event names. Use it
|
|
45
|
+
only when the app wants Julewire to consider every Rails event.
|
|
46
|
+
|
|
47
|
+
Capture sub-options:
|
|
48
|
+
|
|
49
|
+
| Option | Default | Purpose |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| `*.headers` | `false` | `false`, `true`, or explicit header names. |
|
|
52
|
+
| `*.body` | `false` | `true` captures raw buffered bodies; `:json` parses bodies into `*_body_json`. |
|
|
53
|
+
| `*.body_bytes` | `65_536` | Body byte cap; `nil` means no cap. |
|
|
54
|
+
| `*.body_content_types` | `Julewire::Rack::Capture::BodyContentType::JSON_ONLY` | Media-type allowlist; `true` means all non-binary buffered bodies. |
|
|
55
|
+
|
|
56
|
+
## Structured Events
|
|
57
|
+
|
|
58
|
+
| Option | Default | Purpose |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| `structured_event_prefixes` | `["action_controller.", "action_dispatch.", "active_record."]` | Rails event prefixes. |
|
|
61
|
+
| `structured_event_names` | `[]` | Exact event names to emit. |
|
|
62
|
+
| `structured_event_exclude_prefixes` | `[]` | Prefixes to suppress. |
|
|
63
|
+
| `structured_event_exclude_names` | `[]` | Exact names to suppress. |
|
|
64
|
+
|
|
65
|
+
`action_view.` is opt-in because Rails exposes every render start, partial,
|
|
66
|
+
collection, layout, and template as structured events.
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
Julewire::Rails.configure do |config|
|
|
70
|
+
config.structured_event_names = %w[action_view.render_template]
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
Julewire::Rails.configure do |config|
|
|
78
|
+
config.carry_request_headers = %w[traceparent tracestate]
|
|
79
|
+
config.request_exclude_prefixes = ["/julewire"]
|
|
80
|
+
config.response_capture.body = :json
|
|
81
|
+
config.response_capture.body_content_types = %w[application/json]
|
|
82
|
+
end
|
|
83
|
+
```
|
data/docs/development.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Development
|
|
2
|
+
|
|
3
|
+
Run the normal gem checks:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
bundle exec rake
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Run the Rails appraisal suite:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bundle exec rake appraisal:test
|
|
13
|
+
BUNDLE_GEMFILE=gemfiles/rails_8_1.gemfile bundle exec rake test
|
|
14
|
+
BUNDLE_GEMFILE=gemfiles/rails_head.gemfile bundle exec rake test
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The real-stack tests live under `test/dummy` and exercise a booted Rails app,
|
|
18
|
+
default log-subscriber silencing, Rails structured events, logger calls, request
|
|
19
|
+
summaries, and body-capture media gates. Add future Rails versions by adding
|
|
20
|
+
another gemfile under `gemfiles/` and adding it to `RAILS_APPRAISAL_TARGETS`.
|
|
21
|
+
`rails_head` tracks the Rails repository's default branch.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Events and Errors
|
|
2
|
+
|
|
3
|
+
Rails 8.1 structured events are subscribed through `Rails.event`. Controller
|
|
4
|
+
start/completion events enrich the request summary. Framework events such as SQL
|
|
5
|
+
and render notifications are emitted as point records.
|
|
6
|
+
|
|
7
|
+
Rails default text log subscribers are silenced by default when Julewire owns the
|
|
8
|
+
Rails logger. Anything that still reaches `Rails.logger` is treated as a logger
|
|
9
|
+
call, not parsed as duplicate framework text.
|
|
10
|
+
|
|
11
|
+
To keep Rails' default logger output and use Julewire only for request summaries
|
|
12
|
+
or structured-event records:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
Julewire::Rails.configure do |config|
|
|
16
|
+
config.logger = false
|
|
17
|
+
config.request_middleware = true
|
|
18
|
+
config.structured_events = true
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
When `request_summary` is enabled, rendered 4xx/5xx request errors are attached
|
|
23
|
+
to `request.completed`. The summary includes the Rails response status,
|
|
24
|
+
`error_class`, `rescue_response`, and `rescue_template`, plus the core-shaped
|
|
25
|
+
exception.
|
|
26
|
+
|
|
27
|
+
Request-error summary severity inherits Rails'
|
|
28
|
+
`action_dispatch.debug_exception_log_level`. Julewire does not derive this
|
|
29
|
+
severity from the status code.
|
|
30
|
+
|
|
31
|
+
Backtrace policy is owned by core:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
Julewire.configure do |config|
|
|
35
|
+
config.error_backtrace_lines = 0
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Set `rendered_exceptions = true` only when you want an additional diagnostic
|
|
40
|
+
point record from `ActionDispatch::DebugExceptions` alongside the summary-owned
|
|
41
|
+
request error.
|
|
42
|
+
|
|
43
|
+
`Rails.error` remains a separate source for explicit application reports,
|
|
44
|
+
handled reports, jobs, and framework errors that are not owned by a request
|
|
45
|
+
summary. Julewire records those as `rails.error` and shapes the exception
|
|
46
|
+
through core's exception serializer.
|
data/docs/lifecycle.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Lifecycle
|
|
2
|
+
|
|
3
|
+
When `lifecycle_hooks` is enabled, the Railtie installs an at-exit drain hook
|
|
4
|
+
that flushes and closes Julewire with `shutdown_timeout`.
|
|
5
|
+
|
|
6
|
+
It also registers a Rails `ActiveSupport::ForkTracker` after-fork hook that:
|
|
7
|
+
|
|
8
|
+
- calls `Julewire.after_fork!`
|
|
9
|
+
- resets request-summary timeout scheduler state
|
|
10
|
+
- clears request-error ownership state
|
|
11
|
+
|
|
12
|
+
This covers Rails process forks that go through Rails' fork tracker.
|
|
13
|
+
|
|
14
|
+
Custom destinations still own queue, retry, delivery, and reopen behavior. The
|
|
15
|
+
Rails hook only gives them lifecycle opportunities.
|
|
16
|
+
|
|
17
|
+
`require_output` checks after Rails initializers that Julewire has at least one
|
|
18
|
+
configured destination when Julewire owns `Rails.logger`.
|
|
19
|
+
|
|
20
|
+
| Value | Behavior |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `:warn` | Warn, but allow no-output mode. |
|
|
23
|
+
| `:raise` | Fail boot if no output is configured. |
|
|
24
|
+
| `false` | Do not check. |
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Request Logging
|
|
2
|
+
|
|
3
|
+
The request middleware replaces `Rails::Rack::Logger`, wraps each Rack request
|
|
4
|
+
in a Julewire execution scope, and emits one request summary when enabled.
|
|
5
|
+
|
|
6
|
+
Rails emits five Julewire record shapes:
|
|
7
|
+
|
|
8
|
+
| Source | Shape |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| Rails structured events | Point records such as `active_record.sql`. |
|
|
11
|
+
| `Rails.logger.*` | Point records with `event: "log"`. |
|
|
12
|
+
| Request boundary | One `request.completed` summary. |
|
|
13
|
+
| `Rails.error` | Explicit `rails.error` point records. |
|
|
14
|
+
| Rendered exception interceptor | Optional diagnostic point records. |
|
|
15
|
+
|
|
16
|
+
`Rails.logger` records intentionally do not honor user-supplied `kind`,
|
|
17
|
+
`execution`, `carry`, `attributes`, or `neutral` keys. Those keys are treated as
|
|
18
|
+
payload data so ordinary logger calls cannot forge execution identity,
|
|
19
|
+
propagation state, or formatter coordination fields.
|
|
20
|
+
|
|
21
|
+
Request ids, method, path, and remote IP stay in `context`. Rails-specific
|
|
22
|
+
routing and diagnostics such as controller, action, format, params, status,
|
|
23
|
+
filtered URL/path, timings, completion, and capture fields live under
|
|
24
|
+
`attributes.rails`.
|
|
25
|
+
|
|
26
|
+
Provider-neutral HTTP fields live in the record's `neutral` section for
|
|
27
|
+
formatters:
|
|
28
|
+
|
|
29
|
+
- `http.request.method`
|
|
30
|
+
- `url.full`
|
|
31
|
+
- `url.path`
|
|
32
|
+
- `user_agent.original`
|
|
33
|
+
- `client.address`
|
|
34
|
+
- `http.response.status_code`
|
|
35
|
+
- `http.response.body.size`
|
|
36
|
+
|
|
37
|
+
The request summary emits at Rack response completion when Rails/Rack exposes a
|
|
38
|
+
completion hook, or when the wrapped response body closes. If neither happens,
|
|
39
|
+
`request_summary_timeout` emits a `request.completion_timeout` warning point
|
|
40
|
+
with `completion_timeout_ms`. It does not finish the request summary from the
|
|
41
|
+
timeout thread; a later body close can still emit the normal summary.
|
|
42
|
+
|
|
43
|
+
`ActionController::Live` child-thread execution does not inherit Julewire
|
|
44
|
+
context by itself. Live needs an explicit bridge through Rails execution state.
|
|
45
|
+
|
|
46
|
+
Julewire Rails supports Rails' default
|
|
47
|
+
`config.action_dispatch.show_exceptions = :all`. `:rescuable` and `:none` let
|
|
48
|
+
some or all exceptions escape to the Rack server and are not supported modes for
|
|
49
|
+
Julewire Rails request logging.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/julewire/rails/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "julewire-rails"
|
|
7
|
+
spec.version = Julewire::Rails::VERSION
|
|
8
|
+
spec.authors = ["Alexander Grebennik"]
|
|
9
|
+
spec.email = ["slbug@users.noreply.github.com", "sl.bug.sl@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Rails logger and request runtime integration for Julewire."
|
|
12
|
+
spec.description = "Rails 8.1+ integration that routes Rails.logger through Julewire and wraps requests in " \
|
|
13
|
+
"execution scopes."
|
|
14
|
+
spec.homepage = "https://github.com/slbug/julewire"
|
|
15
|
+
spec.license = "MIT"
|
|
16
|
+
spec.required_ruby_version = ">= 3.4"
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/slbug/julewire/tree/main/gems/rails"
|
|
19
|
+
spec.metadata["changelog_uri"] = "https://github.com/slbug/julewire/blob/main/gems/rails/CHANGELOG.md"
|
|
20
|
+
|
|
21
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
22
|
+
|
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
|
24
|
+
Dir[
|
|
25
|
+
"CHANGELOG.md",
|
|
26
|
+
"LICENSE.txt",
|
|
27
|
+
"README.md",
|
|
28
|
+
"docs/**/*.md",
|
|
29
|
+
"julewire-rails.gemspec",
|
|
30
|
+
"lib/**/*.rb"
|
|
31
|
+
]
|
|
32
|
+
end
|
|
33
|
+
spec.executables = []
|
|
34
|
+
spec.require_paths = ["lib"]
|
|
35
|
+
|
|
36
|
+
spec.add_dependency "actionpack", ">= 8.1"
|
|
37
|
+
spec.add_dependency "actionview", ">= 8.1"
|
|
38
|
+
spec.add_dependency "julewire-core", ">= 1.0"
|
|
39
|
+
spec.add_dependency "julewire-rack", ">= 1.0"
|
|
40
|
+
spec.add_dependency "julewire-rails_support", ">= 1.0"
|
|
41
|
+
spec.add_dependency "logger", ">= 1.7"
|
|
42
|
+
spec.add_dependency "railties", ">= 8.1"
|
|
43
|
+
spec.add_dependency "zeitwerk", ">= 2.8.1"
|
|
44
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module Julewire
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
def copy_initializer
|
|
11
|
+
template "julewire.rb", "config/initializers/julewire.rb"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Julewire.configure do |config|
|
|
4
|
+
config.destinations.use(:default, output: $stdout) if config.destinations.empty?
|
|
5
|
+
|
|
6
|
+
config.processors.prepend(
|
|
7
|
+
:rails_parameter_filter,
|
|
8
|
+
Rails.application.config.filter_parameters
|
|
9
|
+
)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
Julewire::Rails.configure do |config|
|
|
13
|
+
config.request_summary = true
|
|
14
|
+
config.structured_events = true
|
|
15
|
+
config.error_reports = true
|
|
16
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Rails
|
|
5
|
+
class Configuration
|
|
6
|
+
include Julewire::Core::Integration::Settings
|
|
7
|
+
|
|
8
|
+
setting :logger, default: true, predicate: true
|
|
9
|
+
setting :logger_name, default: "Rails"
|
|
10
|
+
setting :carry_request_headers, default: false, validate: :validate_carry_request_headers
|
|
11
|
+
setting :error_reports, default: true, predicate: true
|
|
12
|
+
setting :filter_event_payloads, default: true, predicate: true
|
|
13
|
+
setting :lifecycle_hooks, default: true, predicate: true
|
|
14
|
+
setting :log_rescued_responses, default: :auto
|
|
15
|
+
setting :reported_exception_logs, default: :auto
|
|
16
|
+
setting(:request_capture, validate: :validate_capture_settings) { Julewire::Rack::Capture::Settings.new }
|
|
17
|
+
setting :request_context, default: true, predicate: true
|
|
18
|
+
setting :request_exclude_prefixes, default: [], validate: :validate_request_exclude_prefixes
|
|
19
|
+
setting :request_middleware, default: true, predicate: true
|
|
20
|
+
setting :require_output, default: :warn
|
|
21
|
+
setting :request_summary, default: true, predicate: true
|
|
22
|
+
setting :request_summary_timeout, default: 30, validate: :validate_request_summary_timeout
|
|
23
|
+
setting :replace_rack_logger, default: true, predicate: true
|
|
24
|
+
setting(:response_capture, validate: :validate_capture_settings) { Julewire::Rack::Capture::Settings.new }
|
|
25
|
+
setting :rendered_exceptions, default: false, predicate: true
|
|
26
|
+
setting :silence_log_subscribers, default: :auto
|
|
27
|
+
setting :shutdown_timeout, default: 1
|
|
28
|
+
setting :source, default: "rails"
|
|
29
|
+
setting :structured_event_exclude_names, default: []
|
|
30
|
+
setting :structured_event_exclude_prefixes, default: []
|
|
31
|
+
setting :structured_event_names, default: []
|
|
32
|
+
setting :structured_event_prefixes, default: %w[action_controller. action_dispatch. active_record.]
|
|
33
|
+
setting :structured_events, default: true, predicate: true
|
|
34
|
+
setting :summary_event, default: "request.completed"
|
|
35
|
+
|
|
36
|
+
def controller_capture?
|
|
37
|
+
request_capture.enabled? || response_capture.enabled?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def silence_log_subscribers?
|
|
41
|
+
return logger? if silence_log_subscribers == :auto
|
|
42
|
+
|
|
43
|
+
!!silence_log_subscribers
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def validate_carry_request_headers(value)
|
|
49
|
+
return value unless value == true
|
|
50
|
+
|
|
51
|
+
raise Error, "carry_request_headers must be an explicit header list"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def validate_request_summary_timeout(value)
|
|
55
|
+
return value if value.nil?
|
|
56
|
+
return value if value.is_a?(Numeric) && value.positive?
|
|
57
|
+
|
|
58
|
+
raise Error, "request_summary_timeout must be nil or a positive Numeric"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def validate_request_exclude_prefixes(value)
|
|
62
|
+
prefixes = Array(value)
|
|
63
|
+
return prefixes if prefixes.all? { it.is_a?(String) && it.start_with?("/") && !it.empty? }
|
|
64
|
+
|
|
65
|
+
raise Error, "request_exclude_prefixes must contain absolute path prefixes"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def validate_capture_settings(settings)
|
|
69
|
+
settings.validate!
|
|
70
|
+
settings
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|