httplog 1.3.1 → 1.5.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.
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]