grape_rails_logger 1.2.3 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1779ba966e376295939d488c527346f2ee0380d77b5f48471e6d5ba8e60683e3
4
- data.tar.gz: bceef6b5f6f3be3a7db4c2a256e52ccbd0170ab6bee98314480778e5d1a5bb77
3
+ metadata.gz: 1309874132392d9a8663be8cdbcb56a4ce095d1e99773ba1ec520e8efa1f85e3
4
+ data.tar.gz: c831b29fd58a6a7128c351ea6cc99031a3ef24129c57d1ec03620fcd92628ba9
5
5
  SHA512:
6
- metadata.gz: 01aed9f8207e56efd5ae7dcc35d61e29a037bc8792503cd6624f891d7a20c5a6139d620e24579d508d45d5b3961940646ca99b47149592ee5fc7af4a0feafdb2
7
- data.tar.gz: 8b89335e9415219f6cfdecd6d07c579064fd890cc483beaa537fd0529e4c1cfe839036fe6bef6c2ffbd21e4059650c42188c80634e95c7316415e225bf63cbcb
6
+ metadata.gz: 739fb2ea500a4dcd09361b109a6c40812c4b986ab4025892d9413460f63eb92f62c28b57537337a4eacbb0b783a79a224ea47f281623d5452c0f2fcbd30ed839
7
+ data.tar.gz: 20a22a11f028bf57eaeb8463906ecc5628baa59e8a059a58099b94a5ef5a4bc0a72b23187d115b06f572c86a17952ce8d9e45d979b4dd28d422c3af2b947031a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.2.4 (2026-11-07)
4
+
5
+ - Cache `ActiveSupport::ParameterFilter` (and legacy `ParameterFilter`) per `filter_parameters` configuration
6
+ - Use shallow param copy for flat hashes before in-place filtering; keep `deep_dup` when nested structures are present
7
+ - Speed request log build: `nested_param_structure?` fast path, frozen downcased filter tokens, `PARAM_EXCEPTIONS` hash lookup, local `payload` in `build_log_data`
8
+ - In `Timings.record_sql_notification`, derive duration from notification args without allocating `Event` when the payload matches the usual five-argument AS::Notifications shape
9
+
3
10
  ## 1.2.3 (2026-11-03)
4
11
 
5
12
  - Keep tracing and request instrumentation from dispatching a failed request more than once
data/README.md CHANGED
@@ -1,15 +1,9 @@
1
1
  # grape_rails_logger
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/grape_rails_logger.svg?v=1.2.3)](https://badge.fury.io/rb/grape_rails_logger) [![Test Status](https://github.com/amkisko/grape_rails_logger.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/grape_rails_logger.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/grape_rails_logger.rb/graph/badge.svg?token=RC5T0Y2Z5A)](https://app.codecov.io/github/amkisko/grape_rails_logger.rb)
3
+ [![Gem Version](https://badge.fury.io/rb/grape_rails_logger.svg?v=1.2.4)](https://badge.fury.io/rb/grape_rails_logger) [![Test Status](https://github.com/amkisko/grape_rails_logger.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/grape_rails_logger.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/grape_rails_logger.rb/graph/badge.svg?token=RC5T0Y2Z5A)](https://app.codecov.io/github/amkisko/grape_rails_logger.rb)
4
4
 
5
5
  Rails-compatible structured logging for Grape APIs with ActiveRecord timing, parameter filtering, and exception tracking.
6
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
7
  ## Installation
14
8
 
15
9
  Add to your Gemfile:
@@ -120,3 +114,11 @@ usr/bin/release.rb
120
114
  ## License
121
115
 
122
116
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
117
+
118
+ ## Sponsors
119
+
120
+ Sponsored by [Kisko Labs](https://www.kiskolabs.com).
121
+
122
+ <a href="https://www.kiskolabs.com">
123
+ <img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
124
+ </a>
@@ -7,6 +7,9 @@ module GrapeRailsLogger
7
7
  # This class is designed to be exception-safe: any error in logging
8
8
  # is caught and logged separately, never breaking the request flow.
9
9
  class GrapeRequestLogSubscriber
10
+ API_ENDPOINT_ENV_KEY = Grape::Env::API_ENDPOINT
11
+ EMPTY_HASH = {}.freeze
12
+
10
13
  # Custom error class to distinguish logging errors from other errors
11
14
  class LoggingError < StandardError
12
15
  attr_reader :original_error
@@ -19,25 +22,33 @@ module GrapeRailsLogger
19
22
 
20
23
  # Key substrings aligned with Rails filter_parameters defaults (key-only; values pass through).
21
24
  FILTERED_PARAMS = %w[passw secret token _key crypt salt certificate otp ssn authorization].freeze
25
+ FILTERED_PARAMS_LOWER = FILTERED_PARAMS.map(&:downcase).freeze
22
26
  PARAM_EXCEPTIONS = %w[controller action format].freeze
27
+ PARAM_EXCEPTIONS_EXCLUDE = PARAM_EXCEPTIONS.each_with_object({}) { |k, h| h[k] = true }.freeze
28
+
29
+ def initialize
30
+ super
31
+ @parameter_filter_config = nil
32
+ @parameter_filter = nil
33
+ end
23
34
 
24
35
  def grape_request(event)
25
36
  return unless event.is_a?(ActiveSupport::Notifications::Event)
26
37
 
27
- env = event.payload[:env]
38
+ payload = event.payload
39
+ env = payload[:env]
28
40
  return unless env.is_a?(Hash)
29
41
 
30
- # Get logger from event payload (passed from middleware)
31
- logger = event.payload[:logger]
42
+ logger = payload[:logger]
32
43
 
33
44
  logged_successfully = false
34
45
  begin
35
- endpoint = env[Grape::Env::API_ENDPOINT] if env.is_a?(Hash)
46
+ endpoint = env[API_ENDPOINT_ENV_KEY]
36
47
  request = (endpoint&.respond_to?(:request) && endpoint.request) ? endpoint.request : nil
37
48
 
38
49
  data = build_log_data(event, request, env)
39
- logged_successfully = log_data(data, event.payload[:exception_object], logger)
40
- event.payload[:logged_successfully] = true if logged_successfully
50
+ logged_successfully = log_data(data, payload[:exception_object], logger)
51
+ payload[:logged_successfully] = true if logged_successfully
41
52
  rescue LoggingError
42
53
  # If logging itself failed, don't try to log again (would cause infinite loop or duplicates)
43
54
  # The error was already logged in safe_log's rescue block
@@ -60,18 +71,22 @@ module GrapeRailsLogger
60
71
  end
61
72
 
62
73
  def build_log_data(event, request, env)
63
- db_runtime = event.payload[:db_runtime] || 0
64
- db_calls = event.payload[:db_calls] || 0
74
+ payload = event.payload
75
+ db_runtime = payload[:db_runtime] || 0
76
+ db_calls = payload[:db_calls] || 0
65
77
  total_runtime = begin
66
78
  event.duration || 0
67
79
  rescue
68
80
  0
69
81
  end
70
82
 
71
- status = extract_status(event) || (event.payload[:exception_object] ? 500 : 200)
83
+ exception_object = payload[:exception_object]
84
+ status = extract_status(event) || (exception_object ? 500 : 200)
72
85
  rails_request = rails_request_for(env)
73
86
  method = request ? safe_string(request.request_method) : safe_string(env["REQUEST_METHOD"])
74
87
  path = request ? safe_string(request.path) : safe_string(env["PATH_INFO"] || env["REQUEST_URI"])
88
+ duration_q = total_runtime.round(2)
89
+ db_q = db_runtime.round(2)
75
90
 
76
91
  {
77
92
  method: method,
@@ -84,9 +99,9 @@ module GrapeRailsLogger
84
99
  host: extract_host(rails_request, request),
85
100
  remote_addr: extract_remote_addr(rails_request, request),
86
101
  request_id: extract_request_id(rails_request, env),
87
- duration: total_runtime.round(2),
88
- db: db_runtime.round(2),
89
- db_calls: db_calls,
102
+ duration: duration_q,
103
+ db: db_q,
104
+ db_calls: Integer(db_calls),
90
105
  params: filter_params(extract_params(request, env))
91
106
  }
92
107
  end
@@ -578,13 +593,13 @@ module GrapeRailsLogger
578
593
  end
579
594
 
580
595
  def extract_params(request, env = nil)
581
- return {} unless env.is_a?(Hash)
596
+ return EMPTY_HASH unless env.is_a?(Hash)
582
597
 
583
- endpoint = env[Grape::Env::API_ENDPOINT]
598
+ endpoint = env[API_ENDPOINT_ENV_KEY]
584
599
  return {} unless endpoint&.respond_to?(:request) && endpoint.request
585
600
 
586
601
  endpoint_params = endpoint.request.params
587
- return {} if endpoint_params.blank?
602
+ return EMPTY_HASH if endpoint_params.blank?
588
603
 
589
604
  params_hash = if endpoint_params.respond_to?(:to_unsafe_h)
590
605
  endpoint_params.to_unsafe_h
@@ -728,22 +743,27 @@ module GrapeRailsLogger
728
743
  end
729
744
 
730
745
  def filter_params(params)
731
- return {} unless params
746
+ return EMPTY_HASH unless params
732
747
  return {} unless params.is_a?(Hash)
733
748
 
734
749
  cleaned = if (filter = rails_parameter_filter)
735
750
  # Create a deep copy since filter modifies in place (Rails 6+)
736
- params_copy = params.respond_to?(:deep_dup) ? params.deep_dup : params.dup
751
+ params_copy =
752
+ if nested_param_structure?(params)
753
+ params.respond_to?(:deep_dup) ? params.deep_dup : params.dup
754
+ else
755
+ params.dup
756
+ end
737
757
  filter.filter(params_copy)
738
758
  else
739
759
  filter_parameters_manually(params)
740
760
  end
741
761
 
742
- cleaned.is_a?(Hash) ? cleaned.reject { |key, _| PARAM_EXCEPTIONS.include?(key.to_s) } : {}
762
+ cleaned.is_a?(Hash) ? cleaned.reject { |key, _| PARAM_EXCEPTIONS_EXCLUDE[key.to_s] } : {}
743
763
  rescue
744
764
  # Don't log - just fallback to manual filtering
745
765
  # Logging happens only in subscriber
746
- filter_parameters_manually(params).reject { |key, _| PARAM_EXCEPTIONS.include?(key.to_s) }
766
+ filter_parameters_manually(params).reject { |key, _| PARAM_EXCEPTIONS_EXCLUDE[key.to_s] }
747
767
  end
748
768
 
749
769
  def rails_parameter_filter
@@ -754,14 +774,23 @@ module GrapeRailsLogger
754
774
  filter_parameters = Rails.application.config.filter_parameters
755
775
  return nil if filter_parameters.blank?
756
776
 
777
+ if filter_parameters.equal?(@parameter_filter_config) && @parameter_filter
778
+ return @parameter_filter
779
+ end
780
+
757
781
  # Prefer ActiveSupport::ParameterFilter (Rails 6.1+)
758
782
  # This is the Rails-native parameter filtering system
759
- if defined?(ActiveSupport::ParameterFilter)
760
- ActiveSupport::ParameterFilter.new(filter_parameters)
761
- # Fall back to ActionDispatch::Http::ParameterFilter (Rails 6.0 and earlier)
762
- elsif defined?(ActionDispatch::Http::ParameterFilter)
763
- ActionDispatch::Http::ParameterFilter.new(filter_parameters)
764
- end
783
+ built =
784
+ if defined?(ActiveSupport::ParameterFilter)
785
+ ActiveSupport::ParameterFilter.new(filter_parameters)
786
+ # Fall back to ActionDispatch::Http::ParameterFilter (Rails 6.0 and earlier)
787
+ elsif defined?(ActionDispatch::Http::ParameterFilter)
788
+ ActionDispatch::Http::ParameterFilter.new(filter_parameters)
789
+ end
790
+ return nil if built.nil?
791
+
792
+ @parameter_filter_config = filter_parameters
793
+ @parameter_filter = built
765
794
  rescue
766
795
  # Log but don't fail - fall back to manual filtering
767
796
  # Don't log - just return nil
@@ -769,6 +798,33 @@ module GrapeRailsLogger
769
798
  nil
770
799
  end
771
800
 
801
+ def nested_param_structure?(params)
802
+ return false unless params.is_a?(Hash)
803
+ return false if params.empty?
804
+
805
+ return true if params.any? { |_, v| v.is_a?(Hash) }
806
+
807
+ params.each_value do |value|
808
+ next unless value.is_a?(Array)
809
+
810
+ return true if value.any? { |el| el.is_a?(Hash) }
811
+ end
812
+
813
+ stack = [params]
814
+ until stack.empty?
815
+ h = stack.pop
816
+ h.each_value do |value|
817
+ case value
818
+ when Hash
819
+ return true
820
+ when Array
821
+ value.each { |el| stack.push(el) if el.is_a?(Hash) }
822
+ end
823
+ end
824
+ end
825
+ false
826
+ end
827
+
772
828
  def filter_parameters_manually(params, depth = 0)
773
829
  return {} unless params
774
830
  return {"[FILTERED]" => "[max_depth_exceeded]"} if depth > 10
@@ -799,8 +855,13 @@ module GrapeRailsLogger
799
855
  end
800
856
 
801
857
  def should_filter_key?(key)
802
- key_lower = key.to_s.downcase
803
- FILTERED_PARAMS.any? { |param| key_lower.include?(param.downcase) }
858
+ key_lower =
859
+ if key.is_a?(String)
860
+ key.downcase
861
+ else
862
+ key.to_s.downcase
863
+ end
864
+ FILTERED_PARAMS_LOWER.any? { |param| key_lower.include?(param) }
804
865
  end
805
866
  end
806
867
  end
@@ -15,7 +15,7 @@ module GrapeRailsLogger
15
15
 
16
16
  def self.reset_db_runtime
17
17
  state = execution_state
18
- state[:grape_db_runtime] = 0
18
+ state[:grape_db_runtime] = 0.0
19
19
  state[:grape_db_calls] = 0
20
20
  end
21
21
 
@@ -41,19 +41,41 @@ module GrapeRailsLogger
41
41
  execution_state[:grape_db_calls] ||= 0
42
42
  end
43
43
 
44
- def self.append_db_runtime(event)
44
+ def self.append_db_runtime(event_or_duration)
45
45
  state = execution_state
46
46
  return unless state[:grape_db_tracking]
47
47
 
48
- state[:grape_db_runtime] = (state[:grape_db_runtime] || 0) + event.duration
48
+ duration =
49
+ if event_or_duration.is_a?(Numeric)
50
+ event_or_duration
51
+ else
52
+ event_or_duration.duration
53
+ end
54
+ duration = duration.to_f if duration.is_a?(Numeric)
55
+ state[:grape_db_runtime] = (state[:grape_db_runtime] || 0) + duration
49
56
  state[:grape_db_calls] = (state[:grape_db_calls] || 0) + 1
50
57
  end
51
58
 
52
59
  # Railtie SQL subscribe entry: skip Event allocation when no Grape request is tracked.
53
60
  def self.record_sql_notification(*args)
54
- return unless grape_request_active?
61
+ state = execution_state
62
+ return unless state[:grape_db_tracking]
55
63
 
56
- append_db_runtime(ActiveSupport::Notifications::Event.new(*args))
64
+ duration = sql_notification_duration(args)
65
+ append_db_runtime(duration.nil? ? ActiveSupport::Notifications::Event.new(*args) : duration)
66
+ end
67
+
68
+ def self.sql_notification_duration(args)
69
+ case args.length
70
+ when 5
71
+ d = args[2] - args[1]
72
+ d.is_a?(Numeric) ? d.to_f : d
73
+ when 1
74
+ arg = args.first
75
+ d = arg.duration if arg.respond_to?(:duration)
76
+ d.is_a?(Numeric) ? d.to_f : d
77
+ end
57
78
  end
79
+ private_class_method :sql_notification_duration
58
80
  end
59
81
  end
@@ -1,3 +1,3 @@
1
1
  module GrapeRailsLogger
2
- VERSION = "1.2.3"
2
+ VERSION = "1.2.4"
3
3
  end
@@ -22,7 +22,7 @@ module GrapeRailsLogger
22
22
  def self.grape_request_active?: () -> bool
23
23
  def self.db_runtime: () -> Float
24
24
  def self.db_calls: () -> Integer
25
- def self.append_db_runtime: (ActiveSupport::Notifications::Event event) -> void
25
+ def self.append_db_runtime: (ActiveSupport::Notifications::Event | Float | Integer event_or_duration) -> void
26
26
  def self.record_sql_notification: (*untyped args) -> void
27
27
  end
28
28
 
@@ -35,6 +35,8 @@ module GrapeRailsLogger
35
35
  end
36
36
 
37
37
  class GrapeRequestLogSubscriber
38
+ EMPTY_HASH: Hash[untyped, untyped]
39
+
38
40
  def initialize: () -> void
39
41
  def grape_request: (ActiveSupport::Notifications::Event event) -> void
40
42
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grape_rails_logger
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.3
4
+ version: 1.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -89,14 +89,14 @@ dependencies:
89
89
  requirements:
90
90
  - - ">="
91
91
  - !ruby/object:Gem::Version
92
- version: 2.2.0
92
+ version: 2.2.9
93
93
  type: :development
94
94
  prerelease: false
95
95
  version_requirements: !ruby/object:Gem::Requirement
96
96
  requirements:
97
97
  - - ">="
98
98
  - !ruby/object:Gem::Version
99
- version: 2.2.0
99
+ version: 2.2.9
100
100
  - !ruby/object:Gem::Dependency
101
101
  name: webmock
102
102
  requirement: !ruby/object:Gem::Requirement