httplog 1.3.1 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/README.md +53 -22
  4. data/httplog.gemspec +7 -4
  5. data/lib/httplog/adapters/ethon.rb +2 -1
  6. data/lib/httplog/adapters/excon.rb +2 -1
  7. data/lib/httplog/adapters/http.rb +5 -3
  8. data/lib/httplog/adapters/httpclient.rb +6 -4
  9. data/lib/httplog/adapters/net_http.rb +13 -2
  10. data/lib/httplog/adapters/patron.rb +2 -1
  11. data/lib/httplog/configuration.rb +27 -21
  12. data/lib/httplog/http_log.rb +138 -49
  13. data/lib/httplog/version.rb +1 -1
  14. metadata +37 -51
  15. data/.gitignore +0 -7
  16. data/.rspec +0 -2
  17. data/.rubocop.yml +0 -55
  18. data/.rubocop_todo.yml +0 -36
  19. data/.ruby-version +0 -1
  20. data/.travis.yml +0 -17
  21. data/Gemfile +0 -4
  22. data/Gemfile.lock +0 -130
  23. data/Guardfile +0 -25
  24. data/MIT-LICENSE +0 -20
  25. data/Rakefile +0 -46
  26. data/gemfiles/http3.gemfile +0 -7
  27. data/gemfiles/http4.gemfile +0 -7
  28. data/gemfiles/http5.gemfile +0 -7
  29. data/gemfiles/rack1.gemfile +0 -7
  30. data/gemfiles/rack2.gemfile +0 -7
  31. data/spec/adapters/ethon_adapter.rb +0 -26
  32. data/spec/adapters/excon_adapter.rb +0 -16
  33. data/spec/adapters/faraday_adapter.rb +0 -59
  34. data/spec/adapters/http_adapter.rb +0 -27
  35. data/spec/adapters/http_base_adapter.rb +0 -39
  36. data/spec/adapters/httparty_adapter.rb +0 -16
  37. data/spec/adapters/httpclient_adapter.rb +0 -31
  38. data/spec/adapters/net_http_adapter.rb +0 -21
  39. data/spec/adapters/open_uri_adapter.rb +0 -19
  40. data/spec/adapters/patron_adapter.rb +0 -36
  41. data/spec/adapters/typhoeus_adapter.rb +0 -28
  42. data/spec/configuration_spec.rb +0 -22
  43. data/spec/lib/http_client_spec.rb +0 -15
  44. data/spec/lib/http_log_spec.rb +0 -320
  45. data/spec/log/.gitkeep +0 -0
  46. data/spec/spec_helper.rb +0 -45
  47. data/spec/support/index.html +0 -8
  48. data/spec/support/index.html.gz +0 -0
  49. data/spec/support/log4r.yml +0 -17
  50. data/spec/support/not_gzipped.html.gz +0 -8
  51. data/spec/support/shared_examples.rb +0 -79
  52. data/spec/support/test.bin +0 -0
  53. data/spec/support/test.pdf +0 -198
  54. data/spec/support/test_server.rb +0 -34
  55. data/spec/support/utf8-invalid.html +0 -0
  56. data/spec/support/utf8.html +0 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4e1ea9c25d6b30a6857ecbf3358e18a5336039ae411b381b33260d9e9b2af01
4
- data.tar.gz: b7d8d584ad7742092d982b299fcfa4b64bc8d8899be9e854e9df2e185d2a5fb4
3
+ metadata.gz: 97cbba2cdb0af09dcf6664e290981b074a40f9c91d81be74a737a746bfc1e612
4
+ data.tar.gz: 228590a39ab68d89d3261f219e2bac4945f4fde315219943cbbacdfc1aaa2922
5
5
  SHA512:
6
- metadata.gz: 8af022be7a67f8686565e1b989ddfdb7711b5643a647b936731f5bf9e176f3c9b628146c35190f592e617840bdba97df00cd1a84e0017e494de1c70a7acf8cdc
7
- data.tar.gz: 9d0471b0c8b86705176d96c3ee116ecc0ed7f7aae39d8660acfff51ffa1f0453b63a368dd934e2efac7e28990e94d2720f5c345fecf01458095cf7085bf0d7f3
6
+ metadata.gz: d857f2dc7142fb7243371afd4d3993b65ced6092ecb6ac361abbd2957bc1884e18131ca707b3e2c9dc694ad9996c09e12ddf7b31f978d8d55def35322159a861
7
+ data.tar.gz: eafbb1ef94500b468083f8073049749e6073d2ec423fadae8f6aca3221624bb914f28a489305f35cd5fb222b247eb81f5b2446f4694f919959267d83c0bfcbe7
data/CHANGELOG.md CHANGED
@@ -1,3 +1,35 @@
1
+ ## 1.5.0 - 2021-05-20
2
+
3
+ * Support for Ruby 2.7 and frozen strings
4
+ * Development dependency updates
5
+ * Dropped support for net/http v3
6
+ * Performance tweaks
7
+ * Fix for RestClient body read issue (WARNING: this may be reverted, see [#105](https://github.com/trusche/httplog/issues/105))
8
+
9
+ ## 1.4.3 - 2020-06-10
10
+
11
+ * Masking `password` parameter by default... doh.
12
+
13
+ ## 1.4.2 - 2020-02-09
14
+
15
+ * Rollback of the previous two releases due to bugs introduced there.
16
+
17
+ ## 1.4.1 - 2020-02-08 - YANKED
18
+
19
+ * [#91](https://github.com/trusche/httplog/pull/91) Fixed bug returning empty response with HTTP gem
20
+
21
+ ## 1.4.0 - 2020-01-19 - YANKED
22
+
23
+ * [#85](https://github.com/trusche/httplog/pull/85) Parse JSON response and apply deep masking
24
+
25
+ ## 1.3.3 - 2019-11-14
26
+
27
+ * [#83](https://github.com/trusche/httplog/pull/83) Support for graylog
28
+
29
+ ## 1.3.1 - 2019-06-07
30
+
31
+ * [#76](https://github.com/trusche/httplog/pull/76) Added configurable logger method
32
+
1
33
  ## 1.3.0 - 2019-05-18
2
34
 
3
35
  * [#74](https://github.com/trusche/httplog/pull/74) Added ability to filter sensitive parameter values in the request (based on [#73](https://github.com/trusche/httplog/pull/73)). Default masking of `password` parameter
data/README.md CHANGED
@@ -2,14 +2,15 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/httplog.svg)](http://badge.fury.io/rb/httplog) [![Build Status](https://travis-ci.org/trusche/httplog.svg?branch=master)](https://travis-ci.org/trusche/httplog) [![Code Climate](https://codeclimate.com/github/trusche/httplog.svg)](https://codeclimate.com/github/trusche/httplog)
4
4
  [![Release Version](https://img.shields.io/github/release/trusche/httplog.svg)](https://img.shields.io/github/release/trusche/httplog.svg)
5
+ <a href="https://www.bearer.sh?ref=httplog"><img src="/bearer-badge.png" height="20px"/></a>
5
6
 
6
7
  Log outgoing HTTP requests made from your application. Helps with debugging pesky API error responses, or just generally understanding what's going on under the hood.
7
8
 
8
- Requires ruby >= 2.4.
9
+ Requires ruby >= 2.5
9
10
 
10
11
  This gem works with the following ruby modules and libraries:
11
12
 
12
- * [Net::HTTP](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/net/http/rdoc/index.html)
13
+ * [Net::HTTP](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/net/http/rdoc/index.html) v4+
13
14
  * [Ethon](https://github.com/typhoeus/ethon)
14
15
  * [Excon](https://github.com/geemus/excon)
15
16
  * [OpenURI](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/open-uri/rdoc/index.html)
@@ -29,6 +30,10 @@ This is very much a development and debugging tool; it is **not recommended** to
29
30
  use this in a production environment as it is monkey-patching the respective HTTP implementations.
30
31
  You have been warned - use at your own risk.
31
32
 
33
+ Httplog is kindly sponsored by <a href="https://www.bearer.sh?ref=httplog">Bearer.sh</a> - go check them out please!
34
+
35
+ <a href="https://www.bearer.sh?ref=httplog"><img src="/bearer-sponsor.png" height="72px" /></a>
36
+
32
37
  ### Installation
33
38
 
34
39
  gem install httplog
@@ -83,8 +88,22 @@ HttpLog.configure do |config|
83
88
  config.url_whitelist_pattern = nil
84
89
  config.url_blacklist_pattern = nil
85
90
 
86
- # Mask the values of sensitive requestparameters
91
+ # Mask sensitive information in request and response JSON data.
92
+ # Enable global JSON masking by setting the parameter to `/.*/`
93
+ config.url_masked_body_pattern = nil
94
+
95
+ # You can specify any custom JSON serializer that implements `load` and `dump` class methods
96
+ # to parse JSON responses
97
+ config.json_parser = JSON
98
+
99
+ # When using graylog, you can supply a formatter here - see below for details
100
+ config.graylog_formatter = nil
101
+
102
+ # Mask the values of sensitive request parameters
87
103
  config.filter_parameters = %w[password]
104
+
105
+ # Customize the prefix with a proc or lambda
106
+ config.prefix = ->{ "[httplog] #{Time.now} " }
88
107
  end
89
108
  ```
90
109
 
@@ -120,6 +139,37 @@ end
120
139
 
121
140
  For more color options please refer to the [rainbow documentation](https://github.com/sickill/rainbow)
122
141
 
142
+ ### Graylog logging
143
+
144
+ If you use Graylog and want to use its search features such as "benchmark:>1 AND method:PUT",
145
+ you can use this configuration:
146
+
147
+ ```ruby
148
+ FORMATTER = Lograge::Formatters::KeyValue.new
149
+
150
+ HttpLog.configure do |config|
151
+ config.logger = <your GELF::Logger>
152
+ config.logger_method = :add
153
+ config.severity = GELF::Levels::DEBUG
154
+ config.graylog_formatter = FORMATTER
155
+ end
156
+ ```
157
+
158
+ You also can use GELF Graylog format this way:
159
+
160
+ ```ruby
161
+ class Lograge::Formatters::Graylog2HttpLog < Lograge::Formatters::Graylog2
162
+ def short_message data
163
+ data[:response_body] = data[:response_body].to_s.byteslice(0, 32_766) unless data[:response_body].blank?
164
+ "[httplog] [#{data[:response_code]}] #{data[:method]} #{data[:url]}"
165
+ end
166
+ end
167
+
168
+ FORMATTER = Lograge::Formatters::Graylog2HttpLog.new
169
+ ```
170
+
171
+ Or define your own class that implements the `call` method
172
+
123
173
  ### Compact logging
124
174
 
125
175
  If the log is too noisy for you, but you don't want to completely disable it either, set the `compact_log` option to `true`. This will log each request in a single line with method, request URI, response status and time, but no data or headers. No need to disable any other options individually.
@@ -218,9 +268,6 @@ a suggestion for a fix, please open an issue or, even better, submit a pull requ
218
268
 
219
269
  * Benchmarking only covers the time between starting the HTTP request and receiving the response. It does *not* cover the time it takes to establish the TCP connection.
220
270
 
221
- * When using [REST Client](https://github.com/rest-client/rest-client), POST requests might be missing the requests
222
- data. See #54 for details.
223
-
224
271
  ### Running the specs
225
272
 
226
273
  Make sure you have the necessary dependencies installed by running `bundle install`.
@@ -232,19 +279,3 @@ This will launch a simple rack server on port 9292 and run all tests locally aga
232
279
  If you have any issues with or feature requests for httplog,
233
280
  please [open an issue](https://github.com/trusche/httplog/issues) on GitHub
234
281
  or fork the project and send a pull request. **Please include passing specs with all pull requests.**
235
-
236
- ### Contributors
237
-
238
- Thanks to these fine folks for contributing pull requests:
239
-
240
- * [Doug Johnston](https://github.com/dougjohnston)
241
- * [Eric Cohen](https://github.com/eirc)
242
- * [Nikos Dimitrakopoulos](https://github.com/nikosd)
243
- * [Marcos Hack](https://github.com/marcoshack)
244
- * [Andrew Hammond](https://github.com/andrhamm)
245
- * [Chris Keele](https://github.com/christhekeele)
246
- * [Ryan Souza](https://github.com/ryansouza)
247
- * [Ilya Bondarenko](https://github.com/sedx)
248
- * [Kostas Zacharakis](https://github.com/kzacharakis)
249
- * [Yuri Smirnov](https://github.com/tycooon)
250
- * [Manuel Bustillo Alonso](https://github.com/bustikiller)
data/httplog.gemspec CHANGED
@@ -17,7 +17,8 @@ Gem::Specification.new do |gem|
17
17
  gem.description = "Log outgoing HTTP requests made from your application. Helpful for tracking API calls
18
18
  of third party gems that don't provide their own log output."
19
19
 
20
- gem.files = `git ls-files`.split("\n")
20
+ gem.files = Dir['lib/**/*.rb'] +
21
+ %w(httplog.gemspec README.md CHANGELOG.md)
21
22
  gem.test_files = `git ls-files -- test/*`.split("\n")
22
23
  gem.require_paths = ['lib']
23
24
 
@@ -25,17 +26,19 @@ Gem::Specification.new do |gem|
25
26
 
26
27
  gem.add_development_dependency 'ethon', ['~> 0.11']
27
28
  gem.add_development_dependency 'excon', ['~> 0.60']
28
- gem.add_development_dependency 'faraday', ['~> 0.14']
29
+ gem.add_development_dependency 'faraday', ['~> 1.3']
29
30
  gem.add_development_dependency 'guard-rspec', ['~> 4.7']
30
- gem.add_development_dependency 'http', ['~> 3.0']
31
+ gem.add_development_dependency 'http', ['~> 4.0']
31
32
  gem.add_development_dependency 'httparty', ['~> 0.16']
32
33
  gem.add_development_dependency 'httpclient', ['~> 2.8']
34
+ gem.add_development_dependency 'rest-client', ['~> 2.0']
33
35
  gem.add_development_dependency 'listen', ['~> 3.0']
34
36
  gem.add_development_dependency 'patron', ['~> 0.12']
35
- gem.add_development_dependency 'rake', ['~> 12.3']
37
+ gem.add_development_dependency 'rake', ['~> 13.0']
36
38
  gem.add_development_dependency 'rspec', ['~> 3.7']
37
39
  gem.add_development_dependency 'simplecov', ['~> 0.15']
38
40
  gem.add_development_dependency 'thin', ['~> 1.7']
41
+ gem.add_development_dependency 'oj', ['>= 3.9.2']
39
42
 
40
43
  gem.add_dependency 'rack', ['>= 1.0']
41
44
  gem.add_dependency 'rainbow', ['>= 2.0.0']
@@ -39,7 +39,8 @@ if defined?(Ethon)
39
39
  response_headers: headers.map { |header| header.split(/:\s/) }.to_h,
40
40
  benchmark: bm,
41
41
  encoding: encoding,
42
- content_type: content_type
42
+ content_type: content_type,
43
+ mask_body: HttpLog.masked_body_url?(url)
43
44
  )
44
45
  return_code
45
46
  end
@@ -45,7 +45,8 @@ if defined?(Excon)
45
45
  response_headers: headers,
46
46
  benchmark: bm,
47
47
  encoding: headers['Content-Encoding'],
48
- content_type: headers['Content-Type']
48
+ content_type: headers['Content-Type'],
49
+ mask_body: HttpLog.masked_body_url?(url)
49
50
  )
50
51
  result
51
52
  end
@@ -12,7 +12,8 @@ if defined?(::HTTP::Client) && defined?(::HTTP::Connection)
12
12
  @response = send(orig_request_method, req, options)
13
13
  end
14
14
 
15
- if HttpLog.url_approved?(req.uri)
15
+ uri = req.uri
16
+ if HttpLog.url_approved?(uri)
16
17
  body = if defined?(::HTTP::Request::Body)
17
18
  req.body.respond_to?(:source) ? req.body.source : req.body.instance_variable_get(:@body)
18
19
  else
@@ -21,7 +22,7 @@ if defined?(::HTTP::Client) && defined?(::HTTP::Connection)
21
22
 
22
23
  HttpLog.call(
23
24
  method: req.verb,
24
- url: req.uri,
25
+ url: uri,
25
26
  request_body: body,
26
27
  request_headers: req.headers,
27
28
  response_code: @response.code,
@@ -29,7 +30,8 @@ if defined?(::HTTP::Client) && defined?(::HTTP::Connection)
29
30
  response_headers: @response.headers,
30
31
  benchmark: bm,
31
32
  encoding: @response.headers['Content-Encoding'],
32
- content_type: @response.headers['Content-Type']
33
+ content_type: @response.headers['Content-Type'],
34
+ mask_body: HttpLog.masked_body_url?(uri)
33
35
  )
34
36
 
35
37
  body.rewind if body.respond_to?(:rewind)
@@ -16,12 +16,13 @@ if defined?(::HTTPClient)
16
16
  end
17
17
  end
18
18
 
19
- if HttpLog.url_approved?(req.header.request_uri)
19
+ request_uri = req.header.request_uri
20
+ if HttpLog.url_approved?(request_uri)
20
21
  res = conn.pop
21
22
 
22
23
  HttpLog.call(
23
24
  method: req.header.request_method,
24
- url: req.header.request_uri,
25
+ url: request_uri,
25
26
  request_body: req.body,
26
27
  request_headers: req.headers,
27
28
  response_code: res.status_code,
@@ -29,7 +30,8 @@ if defined?(::HTTPClient)
29
30
  response_headers: res.headers,
30
31
  benchmark: bm,
31
32
  encoding: res.headers['Content-Encoding'],
32
- content_type: res.headers['Content-Type']
33
+ content_type: res.headers['Content-Type'],
34
+ mask_body: HttpLog.masked_body_url?(request_uri)
33
35
  )
34
36
  conn.push(res)
35
37
  end
@@ -41,7 +43,7 @@ if defined?(::HTTPClient)
41
43
  alias orig_create_socket create_socket
42
44
 
43
45
  # up to version 2.6, the method signature is `create_socket(site)`; after that,
44
- # it's `create_socket(hort, port)`
46
+ # it's `create_socket(host, port)`
45
47
  if instance_method(:create_socket).arity == 1
46
48
  def create_socket(site)
47
49
  if HttpLog.url_approved?("#{site.host}:#{site.port}")
@@ -11,19 +11,30 @@ module Net
11
11
  bm = Benchmark.realtime do
12
12
  @response = orig_request(req, body, &block)
13
13
  end
14
+ body_stream = req.body_stream
15
+ request_body = if body_stream
16
+ body_stream.to_s # read and rewind for RestClient::Payload::Base
17
+ body_stream.rewind if body_stream.respond_to?(:rewind) # RestClient::Payload::Base has no method rewind
18
+ body_stream.read
19
+ elsif req.body.nil? || req.body.empty?
20
+ body
21
+ else
22
+ req.body
23
+ end
14
24
 
15
25
  if HttpLog.url_approved?(url) && started?
16
26
  HttpLog.call(
17
27
  method: req.method,
18
28
  url: url,
19
- request_body: req.body.nil? || req.body.empty? ? body : req.body,
29
+ request_body: request_body,
20
30
  request_headers: req.each_header.collect,
21
31
  response_code: @response.code,
22
32
  response_body: @response.body,
23
33
  response_headers: @response.each_header.collect,
24
34
  benchmark: bm,
25
35
  encoding: @response['Content-Encoding'],
26
- content_type: @response['Content-Type']
36
+ content_type: @response['Content-Type'],
37
+ mask_body: HttpLog.masked_body_url?(url)
27
38
  )
28
39
  end
29
40
 
@@ -20,7 +20,8 @@ if defined?(Patron)
20
20
  response_headers: @response.headers,
21
21
  benchmark: bm,
22
22
  encoding: @response.headers['Content-Encoding'],
23
- content_type: @response.headers['Content-Type']
23
+ content_type: @response.headers['Content-Type'],
24
+ mask_body: HttpLog.masked_body_url?(url)
24
25
  )
25
26
  end
26
27
 
@@ -5,6 +5,7 @@ module HttpLog
5
5
  attr_accessor :enabled,
6
6
  :compact_log,
7
7
  :json_log,
8
+ :graylog_formatter,
8
9
  :logger,
9
10
  :logger_method,
10
11
  :severity,
@@ -18,34 +19,39 @@ module HttpLog
18
19
  :log_benchmark,
19
20
  :url_whitelist_pattern,
20
21
  :url_blacklist_pattern,
22
+ :url_masked_body_pattern,
21
23
  :color,
22
24
  :prefix_data_lines,
23
25
  :prefix_response_lines,
24
26
  :prefix_line_numbers,
27
+ :json_parser,
25
28
  :filter_parameters
26
29
 
27
30
  def initialize
28
- @enabled = true
29
- @compact_log = false
30
- @json_log = false
31
- @logger = Logger.new($stdout)
32
- @logger_method = :log
33
- @severity = Logger::Severity::DEBUG
34
- @prefix = LOG_PREFIX
35
- @log_connect = true
36
- @log_request = true
37
- @log_headers = false
38
- @log_data = true
39
- @log_status = true
40
- @log_response = true
41
- @log_benchmark = true
42
- @url_whitelist_pattern = nil
43
- @url_blacklist_pattern = nil
44
- @color = false
45
- @prefix_data_lines = false
46
- @prefix_response_lines = false
47
- @prefix_line_numbers = false
48
- @filter_parameters = []
31
+ @enabled = true
32
+ @compact_log = false
33
+ @json_log = false
34
+ @graylog_formatter = nil
35
+ @logger = Logger.new($stdout)
36
+ @logger_method = :log
37
+ @severity = Logger::Severity::DEBUG
38
+ @prefix = LOG_PREFIX
39
+ @log_connect = true
40
+ @log_request = true
41
+ @log_headers = false
42
+ @log_data = true
43
+ @log_status = true
44
+ @log_response = true
45
+ @log_benchmark = true
46
+ @url_whitelist_pattern = nil
47
+ @url_blacklist_pattern = nil
48
+ @url_masked_body_pattern = nil
49
+ @color = false
50
+ @prefix_data_lines = false
51
+ @prefix_response_lines = false
52
+ @prefix_line_numbers = false
53
+ @json_parser = JSON
54
+ @filter_parameters = %w[password]
49
55
  end
50
56
  end
51
57
  end
@@ -29,8 +29,11 @@ module HttpLog
29
29
  end
30
30
 
31
31
  def call(options = {})
32
+ parse_request(options)
32
33
  if config.json_log
33
34
  log_json(options)
35
+ elsif config.graylog_formatter
36
+ log_graylog(options)
34
37
  elsif config.compact_log
35
38
  log_compact(options[:method], options[:url], options[:response_code], options[:benchmark])
36
39
  else
@@ -40,7 +43,7 @@ module HttpLog
40
43
  HttpLog.log_status(options[:response_code])
41
44
  HttpLog.log_benchmark(options[:benchmark])
42
45
  HttpLog.log_headers(options[:response_headers])
43
- HttpLog.log_body(options[:response_body], options[:encoding], options[:content_type])
46
+ HttpLog.log_body(options[:response_body], options[:mask_body], options[:encoding], options[:content_type])
44
47
  end
45
48
  end
46
49
 
@@ -50,10 +53,14 @@ module HttpLog
50
53
  !config.url_whitelist_pattern || url.to_s.match(config.url_whitelist_pattern)
51
54
  end
52
55
 
56
+ def masked_body_url?(url)
57
+ config.filter_parameters.any? && config.url_masked_body_pattern && url.to_s.match(config.url_masked_body_pattern)
58
+ end
59
+
53
60
  def log(msg)
54
61
  return unless config.enabled
55
62
 
56
- config.logger.public_send(config.logger_method, config.severity, colorize(prefix + msg))
63
+ config.logger.public_send(config.logger_method, config.severity, colorize(prefix + msg.to_s))
57
64
  end
58
65
 
59
66
  def log_connection(host, port = nil)
@@ -89,10 +96,10 @@ module HttpLog
89
96
  log("Benchmark: #{seconds.to_f.round(6)} seconds")
90
97
  end
91
98
 
92
- def log_body(body, encoding = nil, content_type = nil)
99
+ def log_body(body, mask_body, encoding = nil, content_type = nil)
93
100
  return unless config.log_response
94
101
 
95
- data = parse_body(body, encoding, content_type)
102
+ data = parse_body(body, mask_body, encoding, content_type)
96
103
 
97
104
  if config.prefix_response_lines
98
105
  log('Response:')
@@ -104,35 +111,50 @@ module HttpLog
104
111
  log("Response: #{e.message}")
105
112
  end
106
113
 
107
- def parse_body(body, encoding, content_type)
108
- unless text_based?(content_type)
109
- raise BodyParsingError, "(not showing binary data)"
110
- end
114
+ def parse_body(body, mask_body, encoding, content_type)
115
+ raise BodyParsingError, "(not showing binary data)" unless text_based?(content_type)
116
+
111
117
 
112
118
  if body.is_a?(Net::ReadAdapter)
113
119
  # open-uri wraps the response in a Net::ReadAdapter that defers reading
114
- # the content, so the reponse body is not available here.
120
+ # the content, so the response body is not available here.
115
121
  raise BodyParsingError, '(not available yet)'
116
122
  end
117
123
 
118
- if encoding =~ /gzip/ && body && !body.empty?
124
+ body_copy = body.dup
125
+ body_copy = body.to_s if defined?(HTTP::Response::Body) && body.is_a?(HTTP::Response::Body)
126
+ return nil if body_copy.nil? || body_copy.empty?
127
+
128
+
129
+ if encoding =~ /gzip/
119
130
  begin
120
- sio = StringIO.new(body.to_s)
131
+ sio = StringIO.new(body_copy.to_s)
121
132
  gz = Zlib::GzipReader.new(sio)
122
- body = gz.read
133
+ body_copy = gz.read
123
134
  rescue Zlib::GzipFile::Error
124
135
  log("(gzip decompression failed)")
125
136
  end
126
137
  end
127
138
 
128
- utf_encoded(body.to_s, content_type)
139
+ result = utf_encoded(body_copy.to_s, content_type)
140
+
141
+ if mask_body
142
+ if content_type =~ /json/
143
+ result = begin
144
+ masked_data config.json_parser.load(result)
145
+ rescue => e
146
+ 'Failed to mask response body: ' + e.message
147
+ end
148
+ else
149
+ result = masked(result)
150
+ end
151
+ end
152
+ result
129
153
  end
130
154
 
131
155
  def log_data(data)
132
156
  return unless config.log_data
133
157
 
134
- data = utf_encoded(masked(data.dup).to_s) unless data.nil?
135
-
136
158
  if config.prefix_data_lines
137
159
  log('Data:')
138
160
  log_data_lines(data)
@@ -147,38 +169,6 @@ module HttpLog
147
169
  log("#{method.to_s.upcase} #{masked(uri)} completed with status code #{status} in #{seconds.to_f.round(6)} seconds")
148
170
  end
149
171
 
150
- def log_json(data = {})
151
- return unless config.json_log
152
-
153
- data[:response_code] = transform_response_code(data[:response_code]) if data[:response_code].is_a?(Symbol)
154
-
155
- parsed_body = begin
156
- parse_body(data[:response_body], data[:encoding], data[:content_type])
157
- rescue BodyParsingError => e
158
- e.message
159
- end
160
-
161
- if config.compact_log
162
- log({
163
- method: data[:method].to_s.upcase,
164
- url: masked(data[:url]),
165
- response_code: data[:response_code].to_i,
166
- benchmark: data[:benchmark]
167
- }.to_json)
168
- else
169
- log({
170
- method: data[:method].to_s.upcase,
171
- url: masked(data[:url]),
172
- request_body: masked(data[:request_body]),
173
- request_headers: masked(data[:request_headers].to_h),
174
- response_code: data[:response_code].to_i,
175
- response_body: parsed_body,
176
- response_headers: data[:response_headers].to_h,
177
- benchmark: data[:benchmark]
178
- }.to_json)
179
- end
180
- end
181
-
182
172
  def transform_response_code(response_code_name)
183
173
  Rack::Utils::HTTP_STATUS_CODES.detect { |_k, v| v.to_s.casecmp(response_code_name.to_s).zero? }.first
184
174
  end
@@ -199,6 +189,70 @@ module HttpLog
199
189
 
200
190
  private
201
191
 
192
+ def log_json(data = {})
193
+ return unless config.json_log
194
+
195
+ log(
196
+ begin
197
+ dump_json(data)
198
+ rescue
199
+ data[:response_body] = "#{config.json_parser} dump failed"
200
+ data[:request_body] = "#{config.json_parser} dump failed"
201
+ dump_json(data)
202
+ end
203
+ )
204
+ end
205
+
206
+ def dump_json(data)
207
+ config.json_parser.dump(json_payload(data))
208
+ end
209
+
210
+ def log_graylog(data)
211
+ result = json_payload(data)
212
+ begin
213
+ send_to_graylog result
214
+ rescue
215
+ result[:response_body] = 'Graylog JSON dump failed'
216
+ result[:request_body] = 'Graylog JSON dump failed'
217
+ send_to_graylog result
218
+ end
219
+ end
220
+
221
+ def send_to_graylog data
222
+ data.compact!
223
+ config.logger.public_send(config.logger_method, config.severity, config.graylog_formatter.call(data))
224
+ end
225
+
226
+ def json_payload(data = {})
227
+ data[:response_code] = transform_response_code(data[:response_code]) if data[:response_code].is_a?(Symbol)
228
+
229
+ parsed_body = begin
230
+ parse_body(data[:response_body], data[:mask_body], data[:encoding], data[:content_type])
231
+ rescue BodyParsingError => e
232
+ e.message
233
+ end
234
+
235
+ if config.compact_log
236
+ {
237
+ method: data[:method].to_s.upcase,
238
+ url: masked(data[:url]),
239
+ response_code: data[:response_code].to_i,
240
+ benchmark: data[:benchmark]
241
+ }
242
+ else
243
+ {
244
+ method: data[:method].to_s.upcase,
245
+ url: masked(data[:url]),
246
+ request_body: data[:request_body],
247
+ request_headers: masked(data[:request_headers].to_h),
248
+ response_code: data[:response_code].to_i,
249
+ response_body: parsed_body,
250
+ response_headers: data[:response_headers].to_h,
251
+ benchmark: data[:benchmark]
252
+ }
253
+ end
254
+ end
255
+
202
256
  def masked(msg, key=nil)
203
257
  return msg if config.filter_parameters.empty?
204
258
  return msg if msg.nil?
@@ -207,11 +261,14 @@ module HttpLog
207
261
  # in its entirety.
208
262
  return (config.filter_parameters.include?(key.downcase) ? PARAM_MASK : msg) if key
209
263
 
210
- # Otherwise, we'll parse Strings for key=valye pairs...
264
+ # Otherwise, we'll parse Strings for key=value pairs,
265
+ # for name="key"\n value...
211
266
  case msg
212
267
  when *string_classes
213
268
  config.filter_parameters.reduce(msg) do |m,key|
214
- m.to_s.gsub(/(#{key})=[^&]+/i, "#{key}=#{PARAM_MASK}")
269
+ scrubbed = m.to_s.encode('UTF-8', invalid: :replace, undef: :replace)
270
+ scrubbed.to_s.gsub(/(#{key})=[^&]+/i, "#{key}=#{PARAM_MASK}")
271
+ .gsub(/name="#{key}"\s+\K[\s\w]+/, "#{PARAM_MASK}\r\n") # multi-part Faraday
215
272
  end
216
273
  # ...and recurse over hashes
217
274
  when *hash_classes
@@ -222,6 +279,38 @@ module HttpLog
222
279
  end
223
280
  end
224
281
 
282
+ def parse_request(options)
283
+ return if options[:request_body].nil?
284
+
285
+ # Downcase content-type and content-encoding because ::HTTP returns "Content-Type" and "Content-Encoding"
286
+ headers = options[:request_headers].find_all do |header, _|
287
+ %w[content-type Content-Type content-encoding Content-Encoding].include? header
288
+ end.to_h.each_with_object({}) { |(k, v), h| h[k.downcase] = v }
289
+
290
+ copy = options[:request_body].dup
291
+
292
+ options[:request_body] = if text_based?(headers['content-type']) && options[:mask_body]
293
+ begin
294
+ parse_body(copy, options[:mask_body], headers['content-encoding'], headers['content-type'])
295
+ rescue BodyParsingError => e
296
+ log(e.message)
297
+ end
298
+ else
299
+ masked(copy).to_s
300
+ end
301
+ end
302
+
303
+ def masked_data msg
304
+ case msg
305
+ when Hash
306
+ Hash[msg.map { |k, v| [k, config.filter_parameters.include?(k.downcase) ? PARAM_MASK : masked_data(v)] }]
307
+ when Array
308
+ msg.map { |element| masked_data(element) }
309
+ else
310
+ msg
311
+ end
312
+ end
313
+
225
314
  def string_classes
226
315
  @string_classes ||= begin
227
316
  string_classes = [String]