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 +4 -4
- data/CHANGELOG.md +5 -0
- data/lib/api_hammer/faraday/request_logger.rb +80 -33
- data/lib/api_hammer/request_logger.rb +46 -28
- data/lib/api_hammer/version.rb +1 -1
- data/test/faraday_request_logger_test.rb +20 -0
- data/test/request_logger_test.rb +21 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b342d8c401e21b4f1de5a9bca122654c97029575
|
4
|
+
data.tar.gz: 43ebdf3104bb5f99a077b1f5a6865175be99ae0b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 354e49d66f033993d760bb2e465e8166330ec28be9f2bc7cf2d1c385fb887b3c5a06e744f60be19cbb3ed52c84024c41d6603004860628b50ecc37b6f3b94b88
|
7
|
+
data.tar.gz: 38e03dedd7c08c876b1f91cb7fd3b68da53b3dc045e71787d753a653d0952c10d884a6f36d7ca993e9de533b2fc19cd5462daba5131c4847250b2fb2324cfb6c
|
data/CHANGELOG.md
CHANGED
@@ -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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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 &&
|
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
|
-
|
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
|
-
|
27
|
+
log_tags = log_tags.dup if log_tags && log_tags.any?
|
28
28
|
|
29
|
-
status,
|
30
|
-
|
31
|
-
body_proxy = ::Rack::BodyProxy.new(
|
32
|
-
|
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,
|
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,
|
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
|
-
'
|
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
|
-
'
|
77
|
-
'
|
78
|
-
|
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' =>
|
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
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
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
|
-
|
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
|
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) &&
|
116
|
-
@logger.tagged(
|
133
|
+
if @logger.respond_to?(:tagged) && log_tags
|
134
|
+
@logger.tagged(log_tags, &dolog)
|
117
135
|
else
|
118
136
|
dolog.call
|
119
137
|
end
|
data/lib/api_hammer/version.rb
CHANGED
@@ -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
|
data/test/request_logger_test.rb
CHANGED
@@ -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
|
+
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-
|
11
|
+
date: 2014-08-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|