grape_rails_logger 1.1.3
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 +32 -0
- data/LICENSE.md +23 -0
- data/README.md +108 -0
- data/lib/grape_rails_logger/debug_tracer.rb +87 -0
- data/lib/grape_rails_logger/endpoint_patch.rb +15 -0
- data/lib/grape_rails_logger/endpoint_wrapper.rb +99 -0
- data/lib/grape_rails_logger/railtie.rb +58 -0
- data/lib/grape_rails_logger/status_extractor.rb +56 -0
- data/lib/grape_rails_logger/subscriber.rb +790 -0
- data/lib/grape_rails_logger/timings.rb +36 -0
- data/lib/grape_rails_logger/version.rb +3 -0
- data/lib/grape_rails_logger.rb +78 -0
- data/sig/grape_rails_logger.rbs +47 -0
- metadata +257 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2d1839d07980cee564c002812c19ab9a6a016e329c6607e0efd559631fee199c
|
|
4
|
+
data.tar.gz: 1a5e41ca402611b34b8784593b7153ead03aa7e6dcdc2b6296bfe346f97d8da7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 50480cc2aaf96b6ac140313ac150da9787913c083bb4925347540eb66d011831b6fd59d1fec128cbf766ff943d7dfd7a6d05c54c04b4392eabff23b25926be68
|
|
7
|
+
data.tar.gz: ac3223da238c2294dfff9bf59b86b88a204282898daf720caaea9e1c89a262f282e44c5b0418a4113f218e7b88e4b82ad0ec2baa2e20320acb037e08e4edecec
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
## 1.1.2 (2025-11-05)
|
|
4
|
+
|
|
5
|
+
- Fix autoloading: replace `require_relative` with `require` in main entry file to work when installed from RubyGems
|
|
6
|
+
- Fix Railtie discovery: always require Railtie file (not conditionally) so Rails can discover it during initialization
|
|
7
|
+
|
|
8
|
+
## 1.1.1 (2025-11-05)
|
|
9
|
+
|
|
10
|
+
- Fix autoloading: explicitly set `require_paths` in gemspec so gem works without explicit `require` parameter in Gemfile
|
|
11
|
+
|
|
12
|
+
## 1.1.0 (2025-11-05)
|
|
13
|
+
|
|
14
|
+
- Removed `GrapeInstrumentation` middleware - instrumentation now happens automatically via Railtie
|
|
15
|
+
- Refactored internal code into focused modules (Timings, StatusExtractor, Subscriber, EndpointPatch, EndpointWrapper)
|
|
16
|
+
- Improved test coverage for core modules
|
|
17
|
+
|
|
18
|
+
## 1.0.0 (2025-11-04)
|
|
19
|
+
|
|
20
|
+
Initial stable release.
|
|
21
|
+
|
|
22
|
+
- Automatic request logging for Grape API endpoints
|
|
23
|
+
- Structured logging with method, path, status, duration, and database timings
|
|
24
|
+
- Automatic parameter filtering for sensitive data (passwords, secrets, tokens)
|
|
25
|
+
- Exception logging with error details and backtraces
|
|
26
|
+
- Debug tracing mode for detailed request inspection (when TRACE env var is set)
|
|
27
|
+
- Automatic Rails integration with zero configuration
|
|
28
|
+
- Works standalone or integrates with activesupport-json_logging
|
|
29
|
+
- Supports Rails 6.0 through 8.0+
|
|
30
|
+
- Thread-safe DB timing using IsolatedExecutionState (Rails 7.1+) or Thread.current (Rails 6-7.0)
|
|
31
|
+
- Controller and action extraction from Grape endpoint source locations
|
|
32
|
+
- Source location tracking (file:line) for debugging
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
|
data/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# grape_rails_logger
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/grape_rails_logger) [](https://github.com/amkisko/grape_rails_logger.rb/actions/workflows/ci.yml) [](https://codecov.io/gh/amkisko/grape_rails_logger.rb)
|
|
4
|
+
|
|
5
|
+
Rails-compatible structured logging for Grape APIs with ActiveRecord timing, parameter filtering, and exception tracking.
|
|
6
|
+
|
|
7
|
+
Sponsored by [Kisko Labs](https://www.kiskolabs.com).
|
|
8
|
+
|
|
9
|
+
<a href="https://www.kiskolabs.com">
|
|
10
|
+
<img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
|
|
11
|
+
</a>
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Add to your Gemfile:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "grape_rails_logger"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Run `bundle install` or `gem install grape_rails_logger`.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
The gem works automatically in Rails applications. No configuration needed. It automatically patches `Grape::Endpoint#build_stack` to instrument requests and subscribes to `grape.request` notifications, logging structured data via `Rails.logger`. Works with any Rails logger or integrates with `activesupport-json_logging` for JSON output.
|
|
26
|
+
|
|
27
|
+
## What gets logged
|
|
28
|
+
|
|
29
|
+
Each request logs structured data with:
|
|
30
|
+
- Request metadata: `method`, `path`, `status`, `duration`, `host`, `remote_addr`, `request_id`
|
|
31
|
+
- Route information: `controller`, `action`, `source_location` (file:line)
|
|
32
|
+
- Performance metrics: `duration` (ms), `db` (ActiveRecord query time in ms), `db_calls` (SQL query count)
|
|
33
|
+
- Parameters: `params` (automatically filtered using Rails `filter_parameters`)
|
|
34
|
+
- Exceptions: `exception` object with `class`, `message`, and `backtrace` (non-production only)
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
Configure parameter filtering in `config/initializers/filter_parameter_logging.rb`:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
Rails.application.config.filter_parameters += [
|
|
42
|
+
:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
|
|
43
|
+
]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
When Rails `ParameterFilter` is not available, the gem falls back to manual filtering that detects sensitive patterns (password, secret, token, key) in parameter keys.
|
|
47
|
+
|
|
48
|
+
## Debug tracing
|
|
49
|
+
|
|
50
|
+
Optional `DebugTracer` middleware provides detailed request tracing when the `debug` gem is installed and `TRACE` environment variable is set:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
class API < Grape::API
|
|
54
|
+
use GrapeRailsLogger::DebugTracer
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Enable tracing: `TRACE=1 rails server`. The middleware gracefully degrades if the `debug` gem is not installed.
|
|
59
|
+
|
|
60
|
+
## Compatibility
|
|
61
|
+
|
|
62
|
+
- Rails 6.0, 6.1, 7.0, 7.1, 7.2, 8.0+
|
|
63
|
+
- Grape >= 1.6
|
|
64
|
+
- Ruby >= 2.7
|
|
65
|
+
|
|
66
|
+
In Rails 7.1+, the gem uses `ActiveSupport::IsolatedExecutionState` for improved thread/Fiber safety. In Rails 6-7.0, it falls back to `Thread.current`.
|
|
67
|
+
|
|
68
|
+
## Development
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bundle install
|
|
72
|
+
bundle exec appraisal install
|
|
73
|
+
bundle exec rspec
|
|
74
|
+
bin/appraisals
|
|
75
|
+
bundle exec standardrb --fix
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Contributing
|
|
79
|
+
|
|
80
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/amkisko/grape_rails_logger.rb
|
|
81
|
+
|
|
82
|
+
Contribution policy:
|
|
83
|
+
- New features are not necessarily added to the gem
|
|
84
|
+
- Pull request should have test coverage for affected parts
|
|
85
|
+
- Pull request should have changelog entry
|
|
86
|
+
|
|
87
|
+
Review policy:
|
|
88
|
+
- It might take up to 2 calendar weeks to review and merge critical fixes
|
|
89
|
+
- It might take up to 6 calendar months to review and merge pull request
|
|
90
|
+
- It might take up to 1 calendar year to review an issue
|
|
91
|
+
|
|
92
|
+
## Publishing
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
rm grape_rails_logger-*.gem
|
|
96
|
+
gem build grape_rails_logger.gemspec
|
|
97
|
+
gem push grape_rails_logger-*.gem
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Or use the release script:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
usr/bin/release.sh
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module GrapeRailsLogger
|
|
2
|
+
# Optional middleware for detailed request tracing when TRACE env var is set
|
|
3
|
+
#
|
|
4
|
+
# Requires the 'debug' gem to be installed. If TRACE is not set or Debug class
|
|
5
|
+
# is unavailable, this middleware passes through without tracing.
|
|
6
|
+
#
|
|
7
|
+
# This middleware is completely exception-safe: any error in tracing will
|
|
8
|
+
# cause the middleware to pass through without tracing, never breaking requests.
|
|
9
|
+
#
|
|
10
|
+
# @example Usage
|
|
11
|
+
# class API < Grape::API
|
|
12
|
+
# use GrapeRailsLogger::DebugTracer # Only traces when TRACE=1
|
|
13
|
+
# end
|
|
14
|
+
class DebugTracer < ::Grape::Middleware::Base
|
|
15
|
+
TRACE_ENABLED = ENV["TRACE"]
|
|
16
|
+
|
|
17
|
+
def call!(env)
|
|
18
|
+
# Fast path: if TRACE not enabled, pass through immediately
|
|
19
|
+
return @app.call(env) unless TRACE_ENABLED
|
|
20
|
+
|
|
21
|
+
# Try to trace, but if anything fails, just pass through
|
|
22
|
+
trace_request(env) do
|
|
23
|
+
@app.call(env)
|
|
24
|
+
end
|
|
25
|
+
rescue => e
|
|
26
|
+
# If tracing fails for any reason, log and pass through
|
|
27
|
+
# Never break the request flow
|
|
28
|
+
log_trace_error(e)
|
|
29
|
+
@app.call(env)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def trace_request(env)
|
|
35
|
+
# Check if Debug is available before attempting to use it
|
|
36
|
+
unless defined?(Debug)
|
|
37
|
+
log_debug_unavailable
|
|
38
|
+
return yield
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
request_method = safe_string(env["REQUEST_METHOD"])
|
|
42
|
+
request_path = safe_string(env["PATH_INFO"] || env["REQUEST_PATH"])
|
|
43
|
+
file_prefix = sanitize_file_prefix(request_method, request_path)
|
|
44
|
+
context = [request_method, request_path].compact.join(" ")
|
|
45
|
+
|
|
46
|
+
Debug.new(
|
|
47
|
+
with_sql: true,
|
|
48
|
+
with_stack: false,
|
|
49
|
+
store_file: true,
|
|
50
|
+
file_prefix: file_prefix,
|
|
51
|
+
context: context
|
|
52
|
+
).trace do
|
|
53
|
+
yield
|
|
54
|
+
end
|
|
55
|
+
rescue => e
|
|
56
|
+
# If Debug.new or trace fails, log and continue without tracing
|
|
57
|
+
log_trace_error(e)
|
|
58
|
+
yield
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def sanitize_file_prefix(method, path)
|
|
62
|
+
[method, path].compact.join("_").downcase.gsub(/[^a-z0-9_]+/, "_").slice(0, 100)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def safe_string(value)
|
|
66
|
+
value&.to_s
|
|
67
|
+
rescue
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def log_debug_unavailable
|
|
72
|
+
return unless defined?(Rails) && Rails.logger
|
|
73
|
+
|
|
74
|
+
Rails.logger.error("DebugTracer: Debug class not available. Install debug gem or disable TRACE.")
|
|
75
|
+
rescue
|
|
76
|
+
# Ignore logger errors - never raise from logging
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def log_trace_error(error)
|
|
80
|
+
return unless defined?(Rails) && Rails.logger
|
|
81
|
+
|
|
82
|
+
Rails.logger.error("DebugTracer: Error during trace - #{error.class}: #{error.message}")
|
|
83
|
+
rescue
|
|
84
|
+
# Silently fail - debug tracing should never break requests
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module GrapeRailsLogger
|
|
2
|
+
# Monkey patch Grape::Endpoint#build_stack to wrap the final Rack app
|
|
3
|
+
# This ensures we capture the response AFTER Error middleware has fully processed it
|
|
4
|
+
module EndpointPatch
|
|
5
|
+
def build_stack(*args)
|
|
6
|
+
app = super
|
|
7
|
+
# Wrap the final Rack app to capture responses after Error middleware
|
|
8
|
+
if ::GrapeRailsLogger.effective_config.enabled
|
|
9
|
+
::GrapeRailsLogger::EndpointWrapper.new(app, self)
|
|
10
|
+
else
|
|
11
|
+
app
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require "logger"
|
|
2
|
+
|
|
3
|
+
module GrapeRailsLogger
|
|
4
|
+
# Wraps the final Rack app returned by Grape::Endpoint#build_stack to capture
|
|
5
|
+
# the response AFTER Error middleware has fully processed it (including rescue_from handlers).
|
|
6
|
+
#
|
|
7
|
+
# This is the correct place to log because:
|
|
8
|
+
# 1. Error middleware wraps everything and processes exceptions
|
|
9
|
+
# 2. rescue_from handlers run and set the correct status
|
|
10
|
+
# 3. The final response is returned with the correct status
|
|
11
|
+
# 4. We capture it here, AFTER all processing is complete
|
|
12
|
+
class EndpointWrapper
|
|
13
|
+
def initialize(app, endpoint)
|
|
14
|
+
@app = app
|
|
15
|
+
@endpoint = endpoint
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(env)
|
|
19
|
+
return @app.call(env) unless GrapeRailsLogger.effective_config.enabled
|
|
20
|
+
|
|
21
|
+
logger = resolve_logger
|
|
22
|
+
start_time = Time.now
|
|
23
|
+
Timings.reset_db_runtime
|
|
24
|
+
|
|
25
|
+
# Wrap the entire request in ActiveSupport::Notifications
|
|
26
|
+
# This ensures we capture the final response AFTER Error middleware processes exceptions
|
|
27
|
+
ActiveSupport::Notifications.instrument("grape.request", env: env, logger: logger) do |payload|
|
|
28
|
+
# Call the wrapped app - Error middleware will process exceptions and return final response
|
|
29
|
+
# NOTE: We do NOT read the body here - Grape will process it and parse params
|
|
30
|
+
# We'll extract params later from already-parsed sources (endpoint.request.params)
|
|
31
|
+
response = @app.call(env)
|
|
32
|
+
|
|
33
|
+
# NOW collect all data AFTER Error middleware has processed exceptions
|
|
34
|
+
# At this point, response contains the final Rack response with correct status
|
|
35
|
+
# AND Grape has already parsed the params, so we can safely access endpoint.request.params
|
|
36
|
+
collect_response_metadata(response, env, payload, start_time)
|
|
37
|
+
|
|
38
|
+
# Return the response - subscriber will log it
|
|
39
|
+
response
|
|
40
|
+
end
|
|
41
|
+
rescue => e
|
|
42
|
+
# If notifications fail, still process the request
|
|
43
|
+
handle_instrumentation_error(e)
|
|
44
|
+
@app.call(env)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def resolve_logger
|
|
50
|
+
config = GrapeRailsLogger.effective_config
|
|
51
|
+
config.logger || (defined?(Rails) && Rails.logger) || Logger.new($stdout)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def collect_response_metadata(response, env, payload, start_time)
|
|
55
|
+
# Extract status from response - this is the FINAL status after Error middleware
|
|
56
|
+
status = extract_status_from_response(response)
|
|
57
|
+
payload[:status] = status if status.is_a?(Integer)
|
|
58
|
+
payload[:response] = response
|
|
59
|
+
|
|
60
|
+
# Extract endpoint info for exception tracking
|
|
61
|
+
endpoint = env[Grape::Env::API_ENDPOINT] if env.is_a?(Hash)
|
|
62
|
+
if endpoint
|
|
63
|
+
# Check for exception info that Grape might have stored
|
|
64
|
+
if endpoint.respond_to?(:options) && endpoint.options.is_a?(Hash)
|
|
65
|
+
exception = endpoint.options[:exception] || endpoint.options["exception"] ||
|
|
66
|
+
endpoint.options[:error] || endpoint.options["error"]
|
|
67
|
+
payload[:exception_object] = exception if exception.is_a?(Exception)
|
|
68
|
+
end
|
|
69
|
+
if endpoint.respond_to?(:exception) && endpoint.exception.is_a?(Exception) && !payload[:exception_object]
|
|
70
|
+
payload[:exception_object] = endpoint.exception
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Capture DB metrics (already collected by Timings module)
|
|
75
|
+
payload[:db_runtime] = Timings.db_runtime
|
|
76
|
+
payload[:db_calls] = Timings.db_calls
|
|
77
|
+
|
|
78
|
+
# Note: The subscriber will extract all other data (method, path, format, etc.)
|
|
79
|
+
# from the env using build_request and other helper methods
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def extract_status_from_response(response)
|
|
83
|
+
return nil unless response
|
|
84
|
+
|
|
85
|
+
if response.is_a?(Array) && response[0].is_a?(Integer)
|
|
86
|
+
response[0]
|
|
87
|
+
elsif response.respond_to?(:to_a)
|
|
88
|
+
array = response.to_a
|
|
89
|
+
array[0] if array.is_a?(Array) && array[0].is_a?(Integer)
|
|
90
|
+
elsif response.respond_to?(:status) && response.status.is_a?(Integer)
|
|
91
|
+
response.status
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def handle_instrumentation_error(error)
|
|
96
|
+
# Silently handle - don't break requests
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Only define Railtie if Rails is available
|
|
2
|
+
# This allows the gem to be loaded even when Rails isn't fully initialized yet
|
|
3
|
+
if defined?(Rails) && defined?(Rails::Railtie)
|
|
4
|
+
require "rails/railtie"
|
|
5
|
+
|
|
6
|
+
module GrapeRailsLogger
|
|
7
|
+
class Railtie < ::Rails::Railtie
|
|
8
|
+
# Add configuration to Rails.application.config
|
|
9
|
+
config.grape_rails_logger = ActiveSupport::OrderedOptions.new
|
|
10
|
+
config.grape_rails_logger.enabled = true
|
|
11
|
+
config.grape_rails_logger.subscriber_class = GrapeRequestLogSubscriber
|
|
12
|
+
config.grape_rails_logger.logger = nil # Default to nil, will use Rails.logger
|
|
13
|
+
config.grape_rails_logger.tag = "Grape" # Default tag for TaggedLogging
|
|
14
|
+
|
|
15
|
+
config.after_initialize do
|
|
16
|
+
# Patch Grape::Endpoint#build_stack to wrap the final Rack app
|
|
17
|
+
# Use prepend to avoid method redefinition issues
|
|
18
|
+
Grape::Endpoint.prepend(GrapeRailsLogger::EndpointPatch)
|
|
19
|
+
# Subscribe to ActiveRecord SQL events for DB timing aggregation
|
|
20
|
+
# Only subscribe if ActiveRecord is loaded (optional dependency)
|
|
21
|
+
if defined?(ActiveRecord)
|
|
22
|
+
ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
|
|
23
|
+
GrapeRailsLogger::Timings.append_db_runtime(ActiveSupport::Notifications::Event.new(*args))
|
|
24
|
+
rescue => e
|
|
25
|
+
# Never let DB timing errors break anything
|
|
26
|
+
# Only warn in development to avoid noise in production
|
|
27
|
+
if Rails.env.development?
|
|
28
|
+
Rails.logger.warn("GrapeRailsLogger: Failed to append DB runtime - #{e.class}: #{e.message}")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Subscribe to Grape request events for logging
|
|
34
|
+
# Only active if Rails.application.config.grape_rails_logger.enabled is true (default: true)
|
|
35
|
+
# This subscription can be disabled by users if they want to handle logging themselves
|
|
36
|
+
# Logging errors are caught and never propagate to avoid breaking requests
|
|
37
|
+
ActiveSupport::Notifications.subscribe("grape.request") do |*args|
|
|
38
|
+
# Check if logging is enabled before processing
|
|
39
|
+
next unless Rails.application.config.grape_rails_logger.enabled
|
|
40
|
+
|
|
41
|
+
begin
|
|
42
|
+
subscriber_class = Rails.application.config.grape_rails_logger.subscriber_class || GrapeRequestLogSubscriber
|
|
43
|
+
subscriber = subscriber_class.new
|
|
44
|
+
subscriber.grape_request(ActiveSupport::Notifications::Event.new(*args))
|
|
45
|
+
rescue => e
|
|
46
|
+
# Last resort: if subscriber creation or invocation fails, log
|
|
47
|
+
# This should never happen, but we're ultra-defensive
|
|
48
|
+
# Only warn in development to avoid noise in production
|
|
49
|
+
if Rails.env.development?
|
|
50
|
+
Rails.logger.warn("GrapeRailsLogger: Subscriber failed - #{e.class}: #{e.message}")
|
|
51
|
+
Rails.logger.warn(e.backtrace&.first(3)&.join("\n")) if e.respond_to?(:backtrace)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module GrapeRailsLogger
|
|
2
|
+
# Shared utility for extracting HTTP status codes from exceptions
|
|
3
|
+
module StatusExtractor
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
# Common exception to status code mappings
|
|
7
|
+
EXCEPTION_STATUS_MAP = {
|
|
8
|
+
"ActiveRecord::RecordNotFound" => 404,
|
|
9
|
+
"ActiveRecord::RecordNotUnique" => 409,
|
|
10
|
+
"ActiveRecord::RecordInvalid" => 422,
|
|
11
|
+
"ActiveRecord::StatementInvalid" => 422,
|
|
12
|
+
"ActionController::RoutingError" => 404,
|
|
13
|
+
"ActionController::MethodNotAllowed" => 405,
|
|
14
|
+
"ActionController::NotImplemented" => 501,
|
|
15
|
+
"ActionController::UnknownFormat" => 406,
|
|
16
|
+
"ActionController::BadRequest" => 400,
|
|
17
|
+
"ActionController::ParameterMissing" => 400
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
# Extracts HTTP status code from an exception
|
|
21
|
+
#
|
|
22
|
+
# @param e [Exception] The exception to extract status from
|
|
23
|
+
# @return [Integer] HTTP status code, defaults to 500
|
|
24
|
+
def extract_status_from_exception(e)
|
|
25
|
+
return e.status if e.respond_to?(:status) && e.status.is_a?(Integer)
|
|
26
|
+
|
|
27
|
+
if e.instance_variable_defined?(:@status)
|
|
28
|
+
status = e.instance_variable_get(:@status)
|
|
29
|
+
return status if status.is_a?(Integer)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if e.respond_to?(:options) && e.options.is_a?(Hash)
|
|
33
|
+
return e.options[:status] if e.options[:status].is_a?(Integer)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Check common exception type mappings
|
|
37
|
+
exception_class_name = e.class.name
|
|
38
|
+
return EXCEPTION_STATUS_MAP[exception_class_name] if EXCEPTION_STATUS_MAP.key?(exception_class_name)
|
|
39
|
+
|
|
40
|
+
# Check if any ancestor class matches
|
|
41
|
+
EXCEPTION_STATUS_MAP.each do |exception_name, status_code|
|
|
42
|
+
# Use safe_constantize if available (ActiveSupport), otherwise constantize
|
|
43
|
+
exception_class = if exception_name.respond_to?(:safe_constantize)
|
|
44
|
+
exception_name.safe_constantize
|
|
45
|
+
else
|
|
46
|
+
exception_name.constantize
|
|
47
|
+
end
|
|
48
|
+
return status_code if exception_class && e.is_a?(exception_class)
|
|
49
|
+
rescue NameError
|
|
50
|
+
# Exception class not available, skip
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
500
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|