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 +4 -4
- data/README.md +5 -0
- data/lib/http_logger/configuration.rb +2 -0
- data/lib/http_logger/version.rb +1 -1
- data/lib/http_logger.rb +95 -6
- data/spec/http_logger_spec.rb +89 -0
- metadata +45 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 293c2066dfe071a200633e80d2ab28fa752ffa1f1cc9863fc1577d7544d36734
|
|
4
|
+
data.tar.gz: 6dfa4fa5c2d60141d5bf35c2f5501170f9cb26e5e63f0a14dc9bc21ed2d53b4b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/http_logger/version.rb
CHANGED
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
|
-
!
|
|
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
|
|
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
|
-
|
|
99
|
-
|
|
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|
|
data/spec/http_logger_spec.rb
CHANGED
|
@@ -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.
|
|
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:
|
|
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:
|
|
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: []
|