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 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
+ [![Gem Version](https://badge.fury.io/rb/grape_rails_logger.svg?v=1.1.0)](https://badge.fury.io/rb/grape_rails_logger) [![Test Status](https://github.com/amkisko/grape_rails_logger.rb/actions/workflows/ci.yml/badge.svg)](https://github.com/amkisko/grape_rails_logger.rb/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/amkisko/grape_rails_logger.rb/graph/badge.svg?token=RC5T0Y2Z5A)](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