api_hammer 0.4.3 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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