api_hammer 0.4.3 → 0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2249d364c2e8a31b69f9e98e284ec2160b2a58ef
4
- data.tar.gz: 1feb8ef43dc7d861f4aa581924a6e217b074abf4
3
+ metadata.gz: b342d8c401e21b4f1de5a9bca122654c97029575
4
+ data.tar.gz: 43ebdf3104bb5f99a077b1f5a6865175be99ae0b
5
5
  SHA512:
6
- metadata.gz: 1842347f2fab562854fb809a7f73dd601cea6cca5cf02ab2f4cc02e2440eb168ac1d85fe65478b9c01af8f6b6f6acfb162ba5d105afcaf779bcd5d5f18c56bf7
7
- data.tar.gz: 1affa946256f016d0e86ada2948f85474e94b93bf8e74944abc71b246aaac88b33118dce62dbf588b1b179d5da42802d5021590bab3b93397cb3684019e03dc0
6
+ metadata.gz: 354e49d66f033993d760bb2e465e8166330ec28be9f2bc7cf2d1c385fb887b3c5a06e744f60be19cbb3ed52c84024c41d6603004860628b50ecc37b6f3b94b88
7
+ data.tar.gz: 38e03dedd7c08c876b1f91cb7fd3b68da53b3dc045e71787d753a653d0952c10d884a6f36d7ca993e9de533b2fc19cd5462daba5131c4847250b2fb2324cfb6c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ # v0.5.0
2
+ - rack request logger logs all request and response headers
3
+ - fix id / uuid / guid logging in rack request logger
4
+ - faraday request logger does not log binary bodies
5
+
1
6
  # 0.4.3
2
7
  - bugfix
3
8
 
@@ -12,6 +12,45 @@ if Faraday::Request.respond_to?(:register_middleware)
12
12
  end
13
13
 
14
14
  module ApiHammer
15
+ # parses attributes out of content type header
16
+ class ContentTypeAttrs
17
+ def initialize(content_type)
18
+ @media_type = content_type.split(/\s*[;]\s*/, 2).first if content_type
19
+ @media_type.strip! if @media_type
20
+ @content_type = content_type
21
+ @parsed = false
22
+ @attributes = Hash.new { |h,k| h[k] = [] }
23
+ catch(:unparseable) do
24
+ throw(:unparseable) unless content_type
25
+ uri_parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
26
+ scanner = StringScanner.new(content_type)
27
+ scanner.scan(/.*;\s*/) || throw(:unparseable)
28
+ while match = scanner.scan(/(\w+)=("?)([^"]*)("?)\s*(,?)\s*/)
29
+ key = scanner[1]
30
+ quote1 = scanner[2]
31
+ value = scanner[3]
32
+ quote2 = scanner[4]
33
+ comma_follows = !scanner[5].empty?
34
+ throw(:unparseable) unless quote1 == quote2
35
+ throw(:unparseable) if !comma_follows && !scanner.eos?
36
+ @attributes[uri_parser.unescape(key)] << uri_parser.unescape(value)
37
+ end
38
+ throw(:unparseable) unless scanner.eos?
39
+ @parsed = true
40
+ end
41
+ end
42
+
43
+ attr_reader :media_type
44
+
45
+ def parsed?
46
+ @parsed
47
+ end
48
+
49
+ def [](key)
50
+ @attributes[key]
51
+ end
52
+ end
53
+
15
54
  class Faraday
16
55
  # Faraday middleware for logging.
17
56
  #
@@ -40,36 +79,15 @@ module ApiHammer
40
79
  response_body.force_encoding('ASCII-8BIT')
41
80
  end
42
81
 
43
- if content_type
44
- # TODO refactor this parsing somewhere better?
45
- parsed = false
46
- attributes = Hash.new { |h,k| h[k] = [] }
47
- catch(:unparseable) do
48
- uri_parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
49
- scanner = StringScanner.new(content_type)
50
- scanner.scan(/.*;\s*/) || throw(:unparseable)
51
- while match = scanner.scan(/(\w+)=("?)([^"]*)("?)\s*(,?)\s*/)
52
- key = scanner[1]
53
- quote1 = scanner[2]
54
- value = scanner[3]
55
- quote2 = scanner[4]
56
- comma_follows = !scanner[5].empty?
57
- throw(:unparseable) unless quote1 == quote2
58
- throw(:unparseable) if !comma_follows && !scanner.eos?
59
- attributes[uri_parser.unescape(key)] << uri_parser.unescape(value)
60
- end
61
- throw(:unparseable) unless scanner.eos?
62
- parsed = true
63
- end
64
- if parsed
65
- charset = attributes['charset'].first
66
- if charset && Encoding.list.any? { |enc| enc.to_s.downcase == charset.downcase }
67
- if response_body.dup.force_encoding(charset).valid_encoding?
68
- response_body.force_encoding(charset)
69
- else
70
- # I guess just ignore the specified encoding if the result is not valid. fall back to
71
- # something else below.
72
- end
82
+ content_type_attrs = ContentTypeAttrs.new(content_type)
83
+ if content_type_attrs.parsed?
84
+ charset = content_type_attrs['charset'].first
85
+ if charset && Encoding.list.any? { |enc| enc.to_s.downcase == charset.downcase }
86
+ if response_body.dup.force_encoding(charset).valid_encoding?
87
+ response_body.force_encoding(charset)
88
+ else
89
+ # I guess just ignore the specified encoding if the result is not valid. fall back to
90
+ # something else below.
73
91
  end
74
92
  end
75
93
  end
@@ -79,7 +97,7 @@ module ApiHammer
79
97
  # if updating by content-type didn't do it, try UTF8 since JSON wants that - but only
80
98
  # if it seems to be valid utf8.
81
99
  # don't try utf8 if the response content-type indicated something else.
82
- try_utf8 = !(parsed && attributes['charset'].any?)
100
+ try_utf8 = !(content_type_attrs && content_type_attrs.parsed? && content_type_attrs['charset'].any?)
83
101
  if try_utf8 && response_body.dup.force_encoding('UTF-8').valid_encoding?
84
102
  response_body.force_encoding('UTF-8')
85
103
  else
@@ -92,6 +110,35 @@ module ApiHammer
92
110
  response_body
93
111
  end
94
112
 
113
+ def text?(content_type)
114
+ content_type_attrs = ContentTypeAttrs.new(content_type)
115
+ media_type = content_type_attrs.media_type
116
+ # ordered hash by priority mapping types to binary or text
117
+ # regexps will have \A and \z added
118
+ types = {
119
+ %r(image/.*) => :binary,
120
+ %r(audio/.*) => :binary,
121
+ %r(video/.*) => :binary,
122
+ %r(model/.*) => :binary,
123
+ %r(text/.*) => :text,
124
+ %r(message/.*) => :text,
125
+ 'application/octet-stream' => :binary,
126
+ 'application/ogg' => :binary,
127
+ 'application/pdf' => :binary,
128
+ 'application/postscript' => :binary,
129
+ 'application/zip' => :binary,
130
+ 'application/gzip' => :binary,
131
+ }
132
+ types.each do |match, type|
133
+ matched = match.is_a?(Regexp) ? media_type =~ %r(\A#{match.source}\z) : media_type == match
134
+ if matched
135
+ return type == :text
136
+ end
137
+ end
138
+ # fallback (unknown or not given) assume text
139
+ return true
140
+ end
141
+
95
142
  def call(request_env)
96
143
  began_at = Time.now
97
144
 
@@ -119,12 +166,12 @@ module ApiHammer
119
166
  'method' => request_env[:method],
120
167
  'uri' => request_env[:url].normalize.to_s,
121
168
  'headers' => request_env.request_headers,
122
- 'body' => request_body,
169
+ 'body' => (request_body if text?(request_env.request_headers['Content-Type'])),
123
170
  }.reject{|k,v| v.nil? },
124
171
  'response' => {
125
172
  'status' => response_env.status,
126
173
  'headers' => response_env.response_headers,
127
- 'body' => response_body(response_env),
174
+ 'body' => (response_body(response_env) if text?(response_env.response_headers['Content-Type'])),
128
175
  }.reject{|k,v| v.nil? },
129
176
  'processing' => {
130
177
  'began_at' => began_at.utc.to_i,
@@ -20,23 +20,25 @@ module ApiHammer
20
20
 
21
21
  # this is closed after the app is called, so read it before
22
22
  env["rack.input"].rewind
23
- @request_body = env["rack.input"].read
23
+ request_body = env["rack.input"].read
24
24
  env["rack.input"].rewind
25
25
 
26
26
  log_tags = Thread.current[:activesupport_tagged_logging_tags]
27
- @log_tags = log_tags.dup if log_tags && log_tags.any?
27
+ log_tags = log_tags.dup if log_tags && log_tags.any?
28
28
 
29
- status, headers, body = @app.call(env)
30
- headers = ::Rack::Utils::HeaderHash.new(headers)
31
- body_proxy = ::Rack::BodyProxy.new(body) { log(env, status, headers, began_at, body) }
32
- [status, headers, body_proxy]
29
+ status, response_headers, response_body = @app.call(env)
30
+ response_headers = ::Rack::Utils::HeaderHash.new(response_headers)
31
+ body_proxy = ::Rack::BodyProxy.new(response_body) do
32
+ log(env, request_body, status, response_headers, response_body, began_at, log_tags)
33
+ end
34
+ [status, response_headers, body_proxy]
33
35
  end
34
36
 
35
- def log(env, status, headers, began_at, body)
37
+ def log(env, request_body, status, response_headers, response_body, began_at, log_tags)
36
38
  now = Time.now
37
39
 
38
40
  request = Rack::Request.new(env)
39
- response = Rack::Response.new('', status, headers)
41
+ response = Rack::Response.new('', status, response_headers)
40
42
 
41
43
  request_uri = Addressable::URI.new(
42
44
  :scheme => request.scheme,
@@ -56,51 +58,67 @@ module ApiHammer
56
58
  :white
57
59
  end
58
60
  status_s = bold(send(status_color, status.to_s))
61
+
62
+ request_headers = env.map do |(key, value)|
63
+ http_match = key.match(/\AHTTP_/)
64
+ if http_match
65
+ name = http_match.post_match.downcase
66
+ {name => value}
67
+ else
68
+ name = %w(content_type content_length).detect { |name| name.downcase == key.downcase }
69
+ if name
70
+ {name => value}
71
+ end
72
+ end
73
+ end.compact.inject({}, &:update)
74
+
59
75
  data = {
60
76
  'request' => {
61
77
  'method' => request.request_method,
62
78
  'uri' => request_uri.normalize.to_s,
63
- 'length' => request.content_length,
64
- 'Content-Type' => request.content_type,
79
+ 'headers' => request_headers,
65
80
  'remote_addr' => env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"],
66
- 'User-Agent' => request.user_agent,
67
81
  # these come from the OAuthenticator gem/middleware
68
82
  'oauth.authenticated' => env['oauth.authenticated'],
69
83
  'oauth.consumer_key' => env['oauth.consumer_key'],
70
84
  'oauth.token' => env['oauth.token'],
71
85
  # airbrake
72
86
  'airbrake.error_id' => env['airbrake.error_id'],
73
- }.reject{|k,v| v.nil? },
87
+ }.reject { |k,v| v.nil? },
74
88
  'response' => {
75
89
  'status' => status,
76
- 'length' => headers['Content-Length'] || body.to_enum.map(&::Rack::Utils.method(:bytesize)).inject(0, &:+),
77
- 'Location' => response.location,
78
- 'Content-Type' => response.content_type,
79
- }.reject{|k,v| v.nil? },
90
+ 'headers' => response_headers,
91
+ 'length' => response_headers['Content-Length'] || response_body.to_enum.map(&::Rack::Utils.method(:bytesize)).inject(0, &:+),
92
+ }.reject { |k,v| v.nil? },
80
93
  'processing' => {
81
94
  'began_at' => began_at.utc.to_i,
82
95
  'duration' => now - began_at,
83
- 'activesupport_tagged_logging_tags' => @log_tags,
84
- }.merge(env['request_logger.info'] || {}).merge(Thread.current['request_logger.info'] || {}).reject{|k,v| v.nil? },
96
+ 'activesupport_tagged_logging_tags' => log_tags,
97
+ }.merge(env['request_logger.info'] || {}).merge(Thread.current['request_logger.info'] || {}).reject { |k,v| v.nil? },
85
98
  }
86
99
  ids_from_body = proc do |body_string, content_type|
87
100
  media_type = ::Rack::Request.new({'CONTENT_TYPE' => content_type}).media_type
88
- if media_type == 'application/json'
89
- body_object = JSON.parse(body_string) rescue nil
90
- # TODO form encoded
101
+ body_object = begin
102
+ if media_type == 'application/json'
103
+ JSON.parse(body_string) rescue nil
104
+ elsif media_type == 'application/x-www-form-urlencoded'
105
+ CGI.parse(body_string).map { |k, vs| {k => vs.last} }.inject({}, &:update)
106
+ end
91
107
  end
92
108
  if body_object.is_a?(Hash)
93
- body_object.reject { |key, value| !(key =~ /\b(uu)?id\b/ && value.is_a?(String)) }
109
+ sep = /(?:\b|\W|_)/
110
+ body_object.reject { |key, value| !(key =~ /#{sep}([ug]u)?id#{sep}/ && value.is_a?(String)) }
94
111
  end
95
112
  end
96
- request_body_ids = ids_from_body.call(@request_body, request.content_type)
97
- data['request']['body_ids'] = request_body_ids if request_body_ids && request_body_ids.any?
98
- response_body_string = body.to_enum.to_a.join('')
113
+ response_body_string = response_body.to_enum.to_a.join('')
99
114
  if (400..599).include?(status.to_i)
100
- # only log response body if there was an error (either client or server)
115
+ # only log bodies if there was an error (either client or server)
116
+ data['request']['body'] = request_body
101
117
  data['response']['body'] = response_body_string
102
118
  else
103
119
  # otherwise, log id and uuid fields
120
+ request_body_ids = ids_from_body.call(request_body, request.content_type)
121
+ data['request']['body_ids'] = request_body_ids if request_body_ids && request_body_ids.any?
104
122
  response_body_ids = ids_from_body.call(response_body_string, response.content_type)
105
123
  data['response']['body_ids'] = response_body_ids if response_body_ids && response_body_ids.any?
106
124
  end
@@ -112,8 +130,8 @@ module ApiHammer
112
130
  @logger.info json_data
113
131
  end
114
132
  # do the logging with tags that applied to the request if appropriate
115
- if @logger.respond_to?(:tagged) && @log_tags
116
- @logger.tagged(@log_tags, &dolog)
133
+ if @logger.respond_to?(:tagged) && log_tags
134
+ @logger.tagged(log_tags, &dolog)
117
135
  else
118
136
  dolog.call
119
137
  end
@@ -1,3 +1,3 @@
1
1
  module ApiHammer
2
- VERSION = "0.4.3"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -104,5 +104,25 @@ describe ApiHammer::RequestLogger do
104
104
  conn.get '/'
105
105
  assert_match('[120,120,195]', logio.string)
106
106
  end
107
+
108
+ {
109
+ 'application/octet-stream' => false,
110
+ 'image/png' => false,
111
+ 'image/png; charset=what' => false,
112
+ 'text/plain' => true,
113
+ 'text/plain; charset=utf-8' => true,
114
+ }.each do |content_type, istext|
115
+ it "does #{istext ? '' : 'not'} log body for #{content_type}" do
116
+ app = proc do |env|
117
+ [200, {'Content-Type' => content_type}, ['data go here']]
118
+ end
119
+ conn = Faraday.new do |f|
120
+ f.request :api_hammer_request_logger, logger
121
+ f.use Faraday::Adapter::Rack, app
122
+ end
123
+ conn.get '/'
124
+ assert(logio.string.include?('data go here') == istext)
125
+ end
126
+ end
107
127
  end
108
128
  end
@@ -20,4 +20,25 @@ describe ApiHammer::RequestLogger do
20
20
  assert(logio.string.include?(Term::ANSIColor.send(color, status.to_s)))
21
21
  end
22
22
  end
23
+
24
+ it 'logs id and uuid (json)' do
25
+ body = %q({"uuid": "theuuid", "foo_uuid": "thefoouuid", "id": "theid", "id_for_x": "theidforx", "bar.id": "thebarid", "baz-guid": "bazzz"})
26
+ app = ApiHammer::RequestLogger.new(proc { |env| [200, {"Content-Type" => 'application/json; charset=UTF8'}, [body]] }, logger)
27
+ app.call(Rack::MockRequest.env_for('/')).last.close
28
+ assert_match(%q("body_ids":{"uuid":"theuuid","foo_uuid":"thefoouuid","id":"theid","id_for_x":"theidforx","bar.id":"thebarid","baz-guid":"bazzz"}), logio.string)
29
+ end
30
+
31
+ it 'logs id and uuid (form encoded)' do
32
+ body = %q(uuid=theuuid&foo_uuid=thefoouuid&id=theid&id_for_x=theidforx&bar.id=thebarid&baz-guid=bazzz)
33
+ app = ApiHammer::RequestLogger.new(proc { |env| [200, {"Content-Type" => 'application/x-www-form-urlencoded; charset=UTF8'}, [body]] }, logger)
34
+ app.call(Rack::MockRequest.env_for('/')).last.close
35
+ assert_match(%q("body_ids":{"uuid":"theuuid","foo_uuid":"thefoouuid","id":"theid","id_for_x":"theidforx","bar.id":"thebarid","baz-guid":"bazzz"}), logio.string)
36
+ end
37
+
38
+ it 'logs request and response body on error' do
39
+ app = ApiHammer::RequestLogger.new(proc { |env| [400, {}, ['the_response_body']] }, logger)
40
+ app.call(Rack::MockRequest.env_for('/', :input => 'the_request_body')).last.close
41
+ assert_match(/"request":\{.*"body":"the_request_body"/, logio.string)
42
+ assert_match(/"response":\{.*"body":"the_response_body"/, logio.string)
43
+ end
23
44
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: api_hammer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ethan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-07-23 00:00:00.000000000 Z
11
+ date: 2014-08-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack