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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +9 -7
- data/lib/grape_rails_logger/subscriber.rb +88 -27
- data/lib/grape_rails_logger/timings.rb +27 -5
- data/lib/grape_rails_logger/version.rb +1 -1
- data/sig/grape_rails_logger.rbs +3 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1309874132392d9a8663be8cdbcb56a4ce095d1e99773ba1ec520e8efa1f85e3
|
|
4
|
+
data.tar.gz: c831b29fd58a6a7128c351ea6cc99031a3ef24129c57d1ec03620fcd92628ba9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
[](https://badge.fury.io/rb/grape_rails_logger) [](https://github.com/amkisko/grape_rails_logger.rb/actions/workflows/test.yml) [](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
|
-
|
|
38
|
+
payload = event.payload
|
|
39
|
+
env = payload[:env]
|
|
28
40
|
return unless env.is_a?(Hash)
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
logger = event.payload[:logger]
|
|
42
|
+
logger = payload[:logger]
|
|
32
43
|
|
|
33
44
|
logged_successfully = false
|
|
34
45
|
begin
|
|
35
|
-
endpoint = env[
|
|
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,
|
|
40
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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:
|
|
88
|
-
db:
|
|
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
|
|
596
|
+
return EMPTY_HASH unless env.is_a?(Hash)
|
|
582
597
|
|
|
583
|
-
endpoint = env[
|
|
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
|
|
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
|
|
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 =
|
|
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, _|
|
|
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, _|
|
|
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
|
-
|
|
760
|
-
ActiveSupport::ParameterFilter
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
ActionDispatch::Http::ParameterFilter
|
|
764
|
-
|
|
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 =
|
|
803
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
61
|
+
state = execution_state
|
|
62
|
+
return unless state[:grape_db_tracking]
|
|
55
63
|
|
|
56
|
-
|
|
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
|
data/sig/grape_rails_logger.rbs
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
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.
|
|
99
|
+
version: 2.2.9
|
|
100
100
|
- !ruby/object:Gem::Dependency
|
|
101
101
|
name: webmock
|
|
102
102
|
requirement: !ruby/object:Gem::Requirement
|