lograge 0.15.0 → 0.16.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7a5c4ad171a2f28dcf7271eac87818900fac9c3f3a1cac94f3a15c7b0df90ba
4
- data.tar.gz: b9ea79a6aff265e3e0c12a52b5e62ec05a4fe4cac8df1c1de9868ddccca0c278
3
+ metadata.gz: 7417503c9af3927cc3291f8030b9cc506d42d8756538dc656218d5a2249f977d
4
+ data.tar.gz: a09ba47fcf99837649481b588b7eb1d90714a7ef3e454d18e09b1b3d894bbfb4
5
5
  SHA512:
6
- metadata.gz: 5d39e78da6b73c9926721a7f121ed4a6697e3b89cc8e018d0d7d622ab205ea50374cf4b3390cea348bee12af380fe1c9985b1a26fb8b454df26cbb9b66da29ec
7
- data.tar.gz: bbaacef289d4d2a0c710c186faeb6c0deca4d161d2ccbdb792c9ba3b0253e26e075f5de7a56b143bc37becb70d8f8abea0834278075b049a66e6269dfda7a9b3
6
+ metadata.gz: b171ecf0bba8e016a819606cfd579cf96b4e790b95ff0a6aba75d1ba3e1a3b180f12ef279bade671d09e1f0aca9090d61ef8bb051674ec60255f58c95492ca7e
7
+ data.tar.gz: 7e537bde3728955c50e6a142c43d126fafeb69ded6d7a8b9ce18410e311be49a166924ba5090f5a7279b156b0b028e9f113616d39c953fc0f39c2cfd9180dd4a
@@ -4,7 +4,7 @@ module Lograge
4
4
  module Formatters
5
5
  class Cee
6
6
  def call(data)
7
- "@cee: #{JSON.dump(data)}"
7
+ "@cee: #{Helpers::SafeJson.dump(data)}"
8
8
  end
9
9
  end
10
10
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Lograge
6
+ module Formatters
7
+ module Helpers
8
+ # Serializes log data to JSON without dropping the whole log event when
9
+ # the data cannot be generated as-is. Newer versions of the json gem are
10
+ # stricter about their input:
11
+ #
12
+ # * json 3.1 rejects non-ASCII ASCII-8BIT (binary) strings, which are
13
+ # common in exception messages and HTTP response bodies.
14
+ # * json 3.0 rejects hashes with keys that serialize to the same name,
15
+ # such as +:status+ and <tt>"status"</tt>.
16
+ #
17
+ # Strings that are invalid UTF-8 have always been rejected.
18
+ #
19
+ # The data is only normalized after the first attempt fails, so well
20
+ # formed events are serialized exactly as before and pay no extra cost.
21
+ module SafeJson
22
+ # ArgumentError is included because, on TruffleRuby, json 3's pure-Ruby
23
+ # generator serializes objects such as Symbols and Times through
24
+ # ActiveSupport's encoder, which before Rails 8.1 passes the
25
+ # +quirks_mode+ option that json 3 no longer accepts. Normalizing turns
26
+ # those objects into plain strings, which avoids that code path.
27
+ GENERATION_ERRORS = [::JSON::GeneratorError, ::EncodingError, ::ArgumentError].freeze
28
+
29
+ module_function
30
+
31
+ def dump(data)
32
+ ::JSON.dump(data)
33
+ rescue *GENERATION_ERRORS
34
+ ::JSON.dump(normalize(data))
35
+ end
36
+
37
+ # Returns a copy of +value+ that the json gem can always generate:
38
+ #
39
+ # * every hash key is a UTF-8 string. When two keys serialize to the same
40
+ # name, the first key keeps its position and the later value wins,
41
+ # which matches how the log subscriber merges custom options.
42
+ # * every string and symbol becomes a valid UTF-8 string. Bytes that
43
+ # cannot be converted are replaced with U+FFFD.
44
+ # * other objects are kept if they can be generated on their own, and
45
+ # are otherwise replaced by their normalized +to_s+ (for example an
46
+ # exception whose message holds binary bytes).
47
+ def normalize(value)
48
+ case value
49
+ when Hash then normalize_hash(value)
50
+ when Array then value.map { |element| normalize(element) }
51
+ when String then to_utf8(value)
52
+ when Symbol then to_utf8(value.to_s)
53
+ when Numeric, true, false, nil then value
54
+ else normalize_object(value)
55
+ end
56
+ end
57
+
58
+ def normalize_hash(hash)
59
+ hash.each_with_object({}) do |(key, value), normalized|
60
+ normalized[to_utf8(key.to_s)] = normalize(value)
61
+ end
62
+ end
63
+
64
+ def normalize_object(object)
65
+ ::JSON.dump(object)
66
+ object
67
+ rescue *GENERATION_ERRORS
68
+ to_utf8(object.to_s)
69
+ end
70
+
71
+ def to_utf8(string)
72
+ encoding = string.encoding
73
+ if encoding == Encoding::UTF_8
74
+ string.valid_encoding? ? string : string.scrub
75
+ elsif encoding == Encoding::BINARY || encoding.dummy?
76
+ # Binary strings carry no encoding information, and dummy encodings
77
+ # (such as UTF-7) have no converter. Treat them as UTF-8 on a
78
+ # best-effort basis so valid multibyte sequences survive.
79
+ string.dup.force_encoding(Encoding::UTF_8).scrub
80
+ else
81
+ string.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -5,7 +5,7 @@ module Lograge
5
5
  module Formatters
6
6
  class Json
7
7
  def call(data)
8
- ::JSON.dump(data)
8
+ Helpers::SafeJson.dump(data)
9
9
  end
10
10
  end
11
11
  end
@@ -32,10 +32,10 @@ module Lograge
32
32
  def escape(string)
33
33
  value = string.is_a?(String) ? string.dup : string.to_s
34
34
 
35
- value.gsub!('\\', '\\\\')
36
- value.gsub!('\n', '\\n')
37
- value.gsub!('\r', '\\r')
38
- value.gsub!('\t', '\\t')
35
+ value.gsub!('\\') { '\\\\' }
36
+ value.gsub!("\n", '\\n')
37
+ value.gsub!("\r", '\\r')
38
+ value.gsub!("\t", '\\t')
39
39
 
40
40
  value
41
41
  end
@@ -33,7 +33,7 @@ module Lograge
33
33
  data.merge!(extract_runtimes(event, payload))
34
34
  data.merge!(extract_location)
35
35
  data.merge!(extract_unpermitted_params)
36
- data.merge!(custom_options(event))
36
+ merge_options(data, custom_options(event))
37
37
  end
38
38
 
39
39
  %i[initial_data extract_status extract_runtimes
@@ -70,7 +70,27 @@ module Lograge
70
70
 
71
71
  def custom_options(event)
72
72
  options = Lograge.custom_options(event) || {}
73
- options.merge event.payload[:custom_payload] || {}
73
+ merge_options(options.dup, event.payload[:custom_payload] || {})
74
+ end
75
+
76
+ # Merges +options+ into +data+. A String key overrides an existing Symbol
77
+ # key of the same name (and vice versa) instead of being added alongside
78
+ # it, so the two never end up serialized as duplicate keys.
79
+ def merge_options(data, options)
80
+ options.each do |key, value|
81
+ data[existing_key(data, key)] = value
82
+ end
83
+ data
84
+ end
85
+
86
+ def existing_key(data, key)
87
+ return key if data.key?(key)
88
+
89
+ counterpart = case key
90
+ when Symbol then key.to_s
91
+ when String then key.to_sym
92
+ end
93
+ counterpart && data.key?(counterpart) ? counterpart : key
74
94
  end
75
95
 
76
96
  def before_format(data, payload)
@@ -5,9 +5,7 @@ require 'delegate'
5
5
  module Lograge
6
6
  class SilentLogger < SimpleDelegator
7
7
  %i[debug info warn error fatal unknown].each do |method_name|
8
- # rubocop:disable Lint/EmptyBlock
9
8
  define_method(method_name) { |*_args| }
10
- # rubocop:enable Lint/EmptyBlock
11
9
  end
12
10
  end
13
11
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lograge
4
- VERSION = '0.15.0'
4
+ VERSION = '0.16.0'
5
5
  end
data/lib/lograge.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'logger'
4
4
  require 'lograge/version'
5
5
  require 'lograge/formatters/helpers/method_and_path'
6
+ require 'lograge/formatters/helpers/safe_json'
6
7
  require 'lograge/formatters/cee'
7
8
  require 'lograge/formatters/json'
8
9
  require 'lograge/formatters/graylog2'
@@ -22,7 +23,6 @@ require 'active_support'
22
23
  require 'active_support/core_ext/module/attribute_accessors'
23
24
  require 'active_support/core_ext/string/inflections'
24
25
 
25
- # rubocop:disable Metrics/ModuleLength
26
26
  module Lograge
27
27
  module_function
28
28
 
@@ -116,17 +116,46 @@ module Lograge
116
116
  unsubscribe(:action_controller, subscriber)
117
117
  end
118
118
  end
119
+
120
+ remove_event_reporter_log_subscriptions
121
+ end
122
+
123
+ # Rails 8.2+ framework log subscribers inherit from
124
+ # ActiveSupport::EventReporter::LogSubscriber and are subscribed to
125
+ # ActiveSupport.event_reporter instead of ActiveSupport::Notifications.
126
+ def remove_event_reporter_log_subscriptions
127
+ rails_event_reporter_log_subscriber_classes.each do |klass|
128
+ ActiveSupport.event_reporter.unsubscribe(klass)
129
+ end
119
130
  end
120
131
 
132
+ def rails_event_reporter_log_subscriber_classes
133
+ return [] unless defined?(ActiveSupport::EventReporter::LogSubscriber)
134
+
135
+ [ActionController::LogSubscriber, ActionView::LogSubscriber].select do |klass|
136
+ klass < ActiveSupport::EventReporter::LogSubscriber
137
+ end
138
+ end
139
+ private_class_method :remove_event_reporter_log_subscriptions, :rails_event_reporter_log_subscriber_classes
140
+
121
141
  def unsubscribe(component, subscriber)
122
142
  events = subscriber.public_methods(false).reject { |method| method.to_s == 'call' }
123
143
  events.each do |event|
124
144
  Lograge.notification_listeners_for("#{event}.#{component}").each do |listener|
125
- ActiveSupport::Notifications.unsubscribe listener if listener.instance_variable_get('@delegate') == subscriber
145
+ ActiveSupport::Notifications.unsubscribe listener if rails_log_subscriber?(listener, subscriber)
126
146
  end
127
147
  end
128
148
  end
129
149
 
150
+ def rails_log_subscriber?(listener, subscriber)
151
+ delegate = listener.instance_variable_get('@delegate')
152
+ return true if delegate == subscriber
153
+
154
+ # Rails 7.1+ attaches a separate ActionView::LogSubscriber::Start listener
155
+ # alongside ActionView::LogSubscriber to log the start of template rendering.
156
+ defined?(ActionView::LogSubscriber::Start) && delegate.is_a?(ActionView::LogSubscriber::Start)
157
+ end
158
+
130
159
  def setup(app)
131
160
  self.application = app
132
161
  disable_rack_cache_verbose_output
@@ -241,6 +270,5 @@ module Lograge
241
270
  end
242
271
  end
243
272
  end
244
- # rubocop:enable Metrics/ModuleLength
245
273
 
246
274
  require 'lograge/railtie' if defined?(Rails)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lograge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mathias Meyer
@@ -25,6 +25,20 @@ dependencies:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
27
  version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: benchmark
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
28
42
  - !ruby/object:Gem::Dependency
29
43
  name: bigdecimal
30
44
  requirement: !ruby/object:Gem::Requirement
@@ -137,6 +151,20 @@ dependencies:
137
151
  - - ">="
138
152
  - !ruby/object:Gem::Version
139
153
  version: '4'
154
+ - !ruby/object:Gem::Dependency
155
+ name: logger
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ type: :runtime
162
+ prerelease: false
163
+ version_requirements: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
140
168
  - !ruby/object:Gem::Dependency
141
169
  name: railties
142
170
  requirement: !ruby/object:Gem::Requirement
@@ -179,6 +207,7 @@ files:
179
207
  - lib/lograge/formatters/cee.rb
180
208
  - lib/lograge/formatters/graylog2.rb
181
209
  - lib/lograge/formatters/helpers/method_and_path.rb
210
+ - lib/lograge/formatters/helpers/safe_json.rb
182
211
  - lib/lograge/formatters/json.rb
183
212
  - lib/lograge/formatters/key_value.rb
184
213
  - lib/lograge/formatters/key_value_deep.rb
@@ -218,7 +247,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
218
247
  - !ruby/object:Gem::Version
219
248
  version: '0'
220
249
  requirements: []
221
- rubygems_version: 4.0.14
250
+ rubygems_version: 3.6.9
222
251
  specification_version: 4
223
252
  summary: Tame Rails' multi-line logging into a single line per request
224
253
  test_files: []