http_logger 1.0.1 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 23dfe1231c8bedcaea4968b27d855c5832249e8977f70315738db6f45ca69d65
4
- data.tar.gz: 1e39712f106bde111c1fca4825ebb25ba03270e2255d9b47aadac7307a153954
3
+ metadata.gz: 293c2066dfe071a200633e80d2ab28fa752ffa1f1cc9863fc1577d7544d36734
4
+ data.tar.gz: 6dfa4fa5c2d60141d5bf35c2f5501170f9cb26e5e63f0a14dc9bc21ed2d53b4b
5
5
  SHA512:
6
- metadata.gz: 7bded89851f6cc5f5b3207f11e1c765f05cf5ece32b42a2be45fcc1b3a6de2e6cea3de6cba53733611df23a2a8265edcf1b99ab95b8eb7c1741afafcb8a32528
7
- data.tar.gz: 7efb4e05af6a5ae4f7f13c0915c9df61b65cba27749786315fcdb7082c3f930b5549e63126c2827f15b9a42673d0f7dd001ae9808661c70e46ffcdcd1f9f0fa0
6
+ metadata.gz: 9e43e1a43adafc86c6c38aca71402340c5370afc74b793c6bff477ce14c1809b8040e0d6c207e0892ce558337eb96203b89a565d67fdd43b071ec6ca687b952e
7
+ data.tar.gz: e4d751fa35fce92194a0cb1058b989f870c391b12f6bfaf6fb3c0de2495f134edfc29ab779378efd60cd8c86f004e54993d98a5afd1bed072e782096d66bd9fc
data/README.md CHANGED
@@ -44,6 +44,11 @@ HttpLogger.configure do |c|
44
44
 
45
45
  # Change default truncate limit. Default: 5000
46
46
  c.collapse_body_limit = 5000
47
+
48
+ # Header names whose values are logged as <filtered>, case-insensitive.
49
+ # Default: ["Authorization"]. Setting it replaces the default rather than
50
+ # adding to it; an empty list logs every header in full.
51
+ c.filtered_headers = %w[Authorization X-Api-Key]
47
52
  end
48
53
  ```
49
54
 
@@ -8,6 +8,7 @@ class HttpLogger
8
8
  attr_accessor :colorize
9
9
  attr_accessor :ignore
10
10
  attr_accessor :level
11
+ attr_accessor :filtered_headers
11
12
 
12
13
  def initialize
13
14
  reset
@@ -21,6 +22,7 @@ class HttpLogger
21
22
  self.collapse_body_limit = 5000
22
23
  self.ignore = []
23
24
  self.level = :debug
25
+ self.filtered_headers = ['Authorization']
24
26
  end
25
27
  end
26
28
  end
@@ -1,3 +1,3 @@
1
1
  class HttpLogger
2
- VERSION = "1.0.1"
2
+ VERSION = "1.0.3"
3
3
  end
data/lib/http_logger.rb CHANGED
@@ -2,6 +2,7 @@ require 'net/http'
2
2
  require 'uri'
3
3
  require 'set'
4
4
  require 'http_logger/configuration'
5
+ require 'http_logger/version'
5
6
 
6
7
  # Usage:
7
8
  #
@@ -48,7 +49,7 @@ class HttpLogger
48
49
  ensure
49
50
  if require_logging?(http, request)
50
51
  log_request_url(http, request, start_time)
51
- log_request_body(request)
52
+ log_request_body(request, request_body)
52
53
  log_request_headers(request)
53
54
  if defined?(response) && response
54
55
  log_response_code(response)
@@ -64,10 +65,17 @@ class HttpLogger
64
65
  content_type = response['Content-Type']
65
66
  return false if content_type.nil?
66
67
 
67
- !content_type.start_with?('text/', 'application/json', 'application/xml', 'application/javascript', 'application/x-www-form-urlencoded', 'application/xhtml+xml', 'application/rss+xml', 'application/atom+xml', 'application/svg+xml', 'application/yaml')
68
+ !textual_content_type?(content_type)
68
69
 
69
70
  end
70
71
 
72
+ def binary_request?(request)
73
+ content_type = request['Content-Type']
74
+ return false if content_type.nil?
75
+
76
+ !textual_content_type?(content_type)
77
+ end
78
+
71
79
  def log_request_url(http, request, start_time)
72
80
  ofset = Time.now - start_time
73
81
  log("HTTP #{request.method} (%0.2fms)" % (ofset * 1000), request_url(http, request))
@@ -86,17 +94,22 @@ class HttpLogger
86
94
  end
87
95
 
88
96
  def log_header(type, name, value)
89
- value = "<filtered>" if name == AUTHORIZATION_HEADER
97
+ value = "<filtered>" if filtered_header?(name)
90
98
  log("HTTP #{type} header", "#{name}: #{value}")
91
99
  end
92
100
 
101
+ def filtered_header?(name)
102
+ Array(configuration.filtered_headers).any? { |header| header.to_s.casecmp?(name.to_s) }
103
+ end
104
+
93
105
  HTTP_METHODS_WITH_BODY = Set.new(%w(POST PUT GET PATCH))
94
106
 
95
- def log_request_body(request)
107
+ def log_request_body(request, request_body = nil)
96
108
  if configuration.log_request_body
97
109
  if HTTP_METHODS_WITH_BODY.include?(request.method)
98
- if (body = request.body) && !body.empty?
99
- log("Request body", truncate_body(body))
110
+ body = request.body || request_body
111
+ if body && !body.empty?
112
+ log("Request body", format_request_body_for_log(request, body))
100
113
  end
101
114
  end
102
115
  end
@@ -197,9 +210,85 @@ class HttpLogger
197
210
  configuration.collapse_body_limit
198
211
  end
199
212
 
213
+ def textual_content_type?(content_type)
214
+ normalized = content_type.to_s.downcase
215
+ normalized.start_with?(
216
+ 'text/',
217
+ 'application/json',
218
+ 'application/xml',
219
+ 'application/javascript',
220
+ 'application/x-www-form-urlencoded',
221
+ 'application/xhtml+xml',
222
+ 'application/rss+xml',
223
+ 'application/atom+xml',
224
+ 'application/svg+xml',
225
+ 'application/yaml',
226
+ )
227
+ end
228
+
200
229
  def configuration
201
230
  self.class.configuration
202
231
  end
232
+
233
+ def format_request_body_for_log(request, body)
234
+ content_type = request['Content-Type']
235
+
236
+ if multipart_content_type?(content_type)
237
+ return truncate_body(sanitize_multipart_binary_parts(body, multipart_boundary(content_type)))
238
+ end
239
+
240
+ binary_request?(request) ? "<binary #{body.bytesize} bytes>" : truncate_body(body)
241
+ end
242
+
243
+ def multipart_content_type?(content_type)
244
+ content_type.to_s.downcase.start_with?('multipart/')
245
+ end
246
+
247
+ def multipart_boundary(content_type)
248
+ match = content_type.to_s.match(/boundary="?([^";]+)"?/i)
249
+ match && match[1]
250
+ end
251
+
252
+ def sanitize_multipart_binary_parts(body, boundary)
253
+ return body unless boundary
254
+
255
+ binary_body = body.dup.force_encoding(Encoding::BINARY)
256
+ delimiter = "--#{boundary}".b
257
+ segments = binary_body.split(delimiter, -1)
258
+ return body if segments.size < 2
259
+
260
+ segments.each_with_index.map do |segment, index|
261
+ if index == 0
262
+ segment
263
+ elsif segment.start_with?("--")
264
+ "#{delimiter}#{segment}"
265
+ else
266
+ "#{delimiter}#{sanitize_multipart_segment(segment)}"
267
+ end
268
+ end.join
269
+ end
270
+
271
+ def sanitize_multipart_segment(segment)
272
+ separator = if segment.include?("\r\n\r\n")
273
+ "\r\n\r\n"
274
+ elsif segment.include?("\n\n")
275
+ "\n\n"
276
+ end
277
+ return segment unless separator
278
+
279
+ headers, part_body = segment.split(separator, 2)
280
+ return segment unless part_body
281
+
282
+ content_type = headers.each_line.find { |line| line.downcase.start_with?('content-type:') }
283
+ return segment unless content_type
284
+
285
+ part_content_type = content_type.split(':', 2).last.to_s.strip
286
+ return segment if textual_content_type?(part_content_type)
287
+
288
+ trailing_newline = part_body.end_with?("\r\n") ? "\r\n" : (part_body.end_with?("\n") ? "\n" : "")
289
+ payload = trailing_newline.empty? ? part_body : part_body[0...-trailing_newline.bytesize]
290
+ "#{headers}#{separator}<binary #{payload.bytesize} bytes>#{trailing_newline}"
291
+ end
203
292
  end
204
293
 
205
294
  block = lambda do |a|
@@ -195,7 +195,96 @@ describe HttpLogger do
195
195
  it { should include("<binary 41887 bytes>") }
196
196
  end
197
197
 
198
+ context "when binary request" do
199
+ let(:url) { "http://example.com/upload" }
200
+ let(:request) do
201
+ http = Net::HTTP.new(uri.host, uri.port)
202
+ request = Net::HTTP::Post.new(uri.path)
203
+ request['Content-Type'] = 'image/jpeg'
204
+ request.body = "\xFF\xD8\xFF\xDBbinarypayload".b
205
+ http.request(request)
206
+ end
207
+
208
+ it { should include("Request body") }
209
+ it { should include("<binary 17 bytes>") }
210
+ it { should_not include("binarypayload") }
211
+ end
212
+
213
+ context "when multipart request has binary and text parts" do
214
+ let(:url) { "http://example.com/upload" }
215
+ let(:boundary) { "turing-export-80cd207c6b0b5f0c" }
216
+ let(:request) do
217
+ http = Net::HTTP.new(uri.host, uri.port)
218
+ request = Net::HTTP::Post.new(uri.path)
219
+ request['Content-Type'] = "multipart/related; boundary=#{boundary}"
220
+ request.body = <<~BODY.chomp
221
+ --#{boundary}
222
+ Content-Type: application/json; charset=UTF-8
223
+
224
+ {"name":"IMG_20190921_152700.jpg","parents":["1LxERY9t0fCAJXJBvBSIGAVhP4IpD0Iyv"]}
225
+ --#{boundary}
226
+ Content-Type: image/jpeg
227
+
228
+ \xFF\xD8\xFF\xDBJPEGDATA
229
+ --#{boundary}--
230
+ BODY
231
+ http.request(request)
232
+ end
233
+
234
+ it { should include(%({"name":"IMG_20190921_152700.jpg","parents":["1LxERY9t0fCAJXJBvBSIGAVhP4IpD0Iyv"]})) }
235
+ it { should include("Content-Type: image/jpeg") }
236
+ it { should include("<binary 12 bytes>") }
237
+ it { should_not include("JPEGDATA") }
238
+ end
239
+
198
240
  after(:each) do
199
241
  HttpLogger.configuration.reset
200
242
  end
243
+
244
+ describe "filtered_headers" do
245
+ before(:each) do
246
+ HttpLogger.configuration.log_headers = true
247
+ end
248
+
249
+ let(:request_headers) do
250
+ {'Authorization' => "Bearer secret", 'X-Api-Key' => "key123"}
251
+ end
252
+
253
+ it "filters Authorization by default and leaves other headers alone" do
254
+ subject.should include("Authorization: <filtered>")
255
+ subject.should include("X-Api-Key: key123")
256
+ end
257
+
258
+ context "with a custom list" do
259
+ before(:each) do
260
+ HttpLogger.configuration.filtered_headers = %w[x-api-key]
261
+ end
262
+
263
+ it "matches case-insensitively and no longer filters Authorization" do
264
+ subject.should include("X-Api-Key: <filtered>")
265
+ subject.should include("Authorization: Bearer secret")
266
+ end
267
+ end
268
+
269
+ context "with an empty list" do
270
+ before(:each) do
271
+ HttpLogger.configuration.filtered_headers = []
272
+ end
273
+
274
+ it "logs every header in full" do
275
+ subject.should include("Authorization: Bearer secret")
276
+ subject.should include("X-Api-Key: key123")
277
+ end
278
+ end
279
+
280
+ context "with response headers" do
281
+ let(:response_headers) { {'Set-Cookie' => "session=abc"} }
282
+
283
+ before(:each) do
284
+ HttpLogger.configuration.filtered_headers = %w[Authorization Set-Cookie]
285
+ end
286
+
287
+ it { should include("Set-Cookie: <filtered>") }
288
+ end
289
+ end
201
290
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: http_logger
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bogdan Gusiev
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-02-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: bundler
@@ -66,6 +65,48 @@ dependencies:
66
65
  - - ">="
67
66
  - !ruby/object:Gem::Version
68
67
  version: '3.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: bigdecimal
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: base64
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: logger
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
69
110
  - !ruby/object:Gem::Dependency
70
111
  name: debug
71
112
  requirement: !ruby/object:Gem::Requirement
@@ -117,7 +158,6 @@ licenses:
117
158
  metadata:
118
159
  source_code_uri: https://github.com/railsware/http_logger
119
160
  changelog_uri: https://github.com/railsware/http_logger/CHANGELOG.md
120
- post_install_message:
121
161
  rdoc_options: []
122
162
  require_paths:
123
163
  - lib
@@ -132,8 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
132
172
  - !ruby/object:Gem::Version
133
173
  version: '0'
134
174
  requirements: []
135
- rubygems_version: 3.5.14
136
- signing_key:
175
+ rubygems_version: 4.0.3
137
176
  specification_version: 4
138
177
  summary: Log your http api calls just like SQL queries
139
178
  test_files: []