rack-traffic-logger 0.2.4 → 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +23 -0
- data/lib/rack/traffic_logger.rb +12 -105
- data/lib/rack/traffic_logger/express_setup.rb +2 -2
- data/lib/rack/traffic_logger/faraday_adapter.rb +50 -0
- data/lib/rack/traffic_logger/formatter/json.rb +1 -1
- data/lib/rack/traffic_logger/formatter/stream.rb +5 -4
- data/lib/rack/traffic_logger/reader.rb +0 -3
- data/lib/rack/traffic_logger/request.rb +104 -0
- data/lib/rack/traffic_logger/stream_simulator.rb +2 -2
- data/lib/rack/traffic_logger/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d4602c2181807d0305806dda404a27aca2715695
|
4
|
+
data.tar.gz: 5fc0fa9b637279c1be407da67f3af6b7e5667190
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c35048dcf21d51d2b727007d0e311dee8cfa4bc42e65fc8302c44c9f6d1b8cf0271da2239cf68e6d54aa7c369292e0b56b048748e2ada9a148ab1e6a5b5a9e7
|
7
|
+
data.tar.gz: 6a7fd9b6535e0976e9bd28b3eed605eda8346c166f421dab5ef1f7b5c06e69e1ecbe0ad273e16ee59ae81f82b1bb0440e56ab547347cd6437647864ea1198722
|
data/README.md
CHANGED
@@ -215,3 +215,26 @@ use Rack::TrafficLogger, 'file.log', Rack::TrafficLogger::Formatter::JSON.new(pr
|
|
215
215
|
```
|
216
216
|
|
217
217
|
Note that if you do, log parsers may have a hard time understanding your logs if they expect each event to be on a single line. If you think this could be an issue, use the first method instead.
|
218
|
+
|
219
|
+
## Usage with Faraday
|
220
|
+
|
221
|
+
If you use [Faraday](https://github.com/lostisland/faraday), you can log outbound HTTP traffic using the included middleware adapter.
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
Faraday.new(url: 'http://localhost') do |builder|
|
225
|
+
builder.use Rack::TrafficLogger::FaradayAdapter, Rails.root.join('log/http_out.log').to_s
|
226
|
+
builder.adapter Faraday.default_adapter
|
227
|
+
end
|
228
|
+
```
|
229
|
+
|
230
|
+
You can also use express setup:
|
231
|
+
|
232
|
+
```ruby
|
233
|
+
Faraday.new(url: 'http://localhost') do |builder|
|
234
|
+
Rack::TrafficLogger::FaradayAdapter.use on: builder,
|
235
|
+
filter: ENV['LOG_OUTBOUND_HTTP'],
|
236
|
+
formatter: Rack::TrafficLogger::Formatter::JSON.new,
|
237
|
+
log_path: Rails.root.join('log/http_out.log').to_s
|
238
|
+
builder.adapter Faraday.default_adapter
|
239
|
+
end
|
240
|
+
```
|
data/lib/rack/traffic_logger.rb
CHANGED
@@ -5,6 +5,8 @@ require_relative 'traffic_logger/stream_simulator'
|
|
5
5
|
require_relative 'traffic_logger/formatter'
|
6
6
|
require_relative 'traffic_logger/reader'
|
7
7
|
require_relative 'traffic_logger/express_setup'
|
8
|
+
require_relative 'traffic_logger/request'
|
9
|
+
require_relative 'traffic_logger/faraday_adapter'
|
8
10
|
|
9
11
|
require 'json'
|
10
12
|
require 'securerandom'
|
@@ -25,7 +27,7 @@ module Rack
|
|
25
27
|
REMOTE_ADDR
|
26
28
|
]
|
27
29
|
|
28
|
-
attr_reader :
|
30
|
+
attr_reader :options
|
29
31
|
|
30
32
|
def initialize(app, log_path, *options)
|
31
33
|
@app = app
|
@@ -35,7 +37,15 @@ module Rack
|
|
35
37
|
end
|
36
38
|
|
37
39
|
def call(env)
|
38
|
-
Request.new(self)
|
40
|
+
request = Request.new(self)
|
41
|
+
request.start env
|
42
|
+
response = nil
|
43
|
+
begin
|
44
|
+
response = @app.call(env)
|
45
|
+
ensure
|
46
|
+
request.finish response
|
47
|
+
end
|
48
|
+
response
|
39
49
|
end
|
40
50
|
|
41
51
|
def log(hash)
|
@@ -50,108 +60,5 @@ module Rack
|
|
50
60
|
end
|
51
61
|
end
|
52
62
|
|
53
|
-
private
|
54
|
-
|
55
|
-
class Request
|
56
|
-
|
57
|
-
def initialize(logger)
|
58
|
-
@logger = logger
|
59
|
-
@id = SecureRandom.hex 4
|
60
|
-
@started_at = Time.now
|
61
|
-
end
|
62
|
-
|
63
|
-
def call(env)
|
64
|
-
@verb = env['REQUEST_METHOD']
|
65
|
-
@env = env
|
66
|
-
begin
|
67
|
-
response = @logger.app.call(env)
|
68
|
-
ensure
|
69
|
-
@code = Array === response ? response.first.to_i : 0
|
70
|
-
@options = @logger.options.for(@verb, @code)
|
71
|
-
if @options.basic?
|
72
|
-
log_request env
|
73
|
-
log_response env, response if @code > 0
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
private
|
79
|
-
|
80
|
-
BASIC_AUTH_PATTERN = /^basic ([a-z\d+\/]+={0,2})$/i
|
81
|
-
|
82
|
-
def log_request(env)
|
83
|
-
log 'request' do |hash|
|
84
|
-
if @options.request_headers?
|
85
|
-
hash.merge! env.reject { |_, v| v.respond_to? :read }
|
86
|
-
else
|
87
|
-
hash.merge! env.select { |k, _| BASIC_ENV_PROPERTIES.include? k }
|
88
|
-
end
|
89
|
-
|
90
|
-
hash['BASIC_AUTH_USERINFO'] = $1.unpack('m').first.split(':', 2) if hash['HTTP_AUTHORIZATION'] =~ BASIC_AUTH_PATTERN
|
91
|
-
|
92
|
-
input = env['rack.input']
|
93
|
-
if input && @options.request_bodies?
|
94
|
-
add_body_to_hash input.tap(&:rewind).read, env['CONTENT_ENCODING'] || env['HTTP_CONTENT_ENCODING'], hash
|
95
|
-
input.rewind
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
def log_response(env, response)
|
101
|
-
code, headers, body = response
|
102
|
-
code = code.to_i
|
103
|
-
headers = HeaderHash.new(headers) if @options.response_headers? || @options.response_bodies?
|
104
|
-
log 'response' do |hash|
|
105
|
-
hash['http_version'] = env['HTTP_VERSION'] || 'HTTP/1.1'
|
106
|
-
hash['status_code'] = code
|
107
|
-
hash['status_name'] = Utils::HTTP_STATUS_CODES[code]
|
108
|
-
hash['headers'] = headers if @options.response_headers?
|
109
|
-
add_body_to_hash get_real_body(body), headers['Content-Encoding'], hash if @options.response_bodies?
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
# Rack allows response bodies to be a few different things. This method
|
114
|
-
# ensures we get a string back.
|
115
|
-
def get_real_body(body)
|
116
|
-
|
117
|
-
# For bodies representing temporary files
|
118
|
-
body = File.open(body.path, 'rb') { |f| f.read } if body.respond_to? :path
|
119
|
-
|
120
|
-
# For bodies representing streams
|
121
|
-
body = body.read.tap { body.rewind } if body.respond_to? :read
|
122
|
-
|
123
|
-
# When body is an array (the common scenario)
|
124
|
-
body = body.join if body.respond_to? :join
|
125
|
-
|
126
|
-
# When body is a proxy
|
127
|
-
body = body.body while Rack::BodyProxy === body
|
128
|
-
|
129
|
-
# It should be a string now. Just in case it's not...
|
130
|
-
body.to_s
|
131
|
-
|
132
|
-
end
|
133
|
-
|
134
|
-
def add_body_to_hash(body, encoding, hash)
|
135
|
-
body = Zlib::GzipReader.new(StringIO.new body).read if encoding == 'gzip'
|
136
|
-
body.force_encoding 'UTF-8'
|
137
|
-
if body.valid_encoding?
|
138
|
-
hash['body'] = body
|
139
|
-
else
|
140
|
-
hash['body_base64'] = [body].pack 'm0'
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
def log(event)
|
145
|
-
hash = {
|
146
|
-
timestamp: Time.now,
|
147
|
-
request_log_id: @id,
|
148
|
-
event: event
|
149
|
-
}
|
150
|
-
yield hash rescue hash.merge! error: $!
|
151
|
-
@logger.log hash
|
152
|
-
end
|
153
|
-
|
154
|
-
end
|
155
|
-
|
156
63
|
end
|
157
64
|
end
|
@@ -1,10 +1,10 @@
|
|
1
1
|
module Rack
|
2
2
|
class TrafficLogger
|
3
3
|
|
4
|
-
def self.use(on: nil, filter:
|
4
|
+
def self.use(on: nil, filter: true, log_path: '/dev/stdout', formatter: nil)
|
5
5
|
filter = (filter || '').to_s.downcase.strip
|
6
6
|
unless ['0', 'no', 'false', 'nil', 'none', '', 'off'].include? filter
|
7
|
-
args = [
|
7
|
+
args = [self, log_path]
|
8
8
|
args << formatter if formatter
|
9
9
|
begin
|
10
10
|
raise if %w[1 yes true normal basic minimal on].include? filter
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Rack
|
2
|
+
class TrafficLogger
|
3
|
+
# noinspection RubyStringKeysInHashInspection
|
4
|
+
class FaradayAdapter < TrafficLogger
|
5
|
+
|
6
|
+
def call(request_env)
|
7
|
+
rack_env = convert_request(request_env)
|
8
|
+
request = Request.new(self)
|
9
|
+
request.start rack_env
|
10
|
+
@app.call(request_env).on_complete do |response_env|
|
11
|
+
rack_response = convert_response(response_env)
|
12
|
+
request.finish rack_response
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def convert_request(faraday_env)
|
19
|
+
url = faraday_env.url
|
20
|
+
{
|
21
|
+
'REQUEST_METHOD' => faraday_env.method.to_s.upcase,
|
22
|
+
'SERVER_NAME' => url.host,
|
23
|
+
'SERVER_PORT' => url.port,
|
24
|
+
'PATH_INFO' => url.path,
|
25
|
+
'HTTP_VERSION' => 'HTTP/1.1', # TODO: can this be obtained?
|
26
|
+
'REMOTE_HOST' => 'localhost',
|
27
|
+
'REMOTE_ADDR' => '127.0.0.1'
|
28
|
+
}.tap do |hash|
|
29
|
+
hash['HTTPS'] = 'on' if url.scheme == 'https'
|
30
|
+
hash['QUERY_STRING'] = url.query if url.query
|
31
|
+
hash.merge!(faraday_env.request_headers.map do |k, v|
|
32
|
+
k = k.gsub('-', '_').upcase
|
33
|
+
k = "HTTP_#{k}" unless k.start_with? 'CONTENT_'
|
34
|
+
[k, v]
|
35
|
+
end.to_h)
|
36
|
+
hash['rack.input'] = StringIO.new(faraday_env.body) if faraday_env.body
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def convert_response(faraday_env)
|
41
|
+
[
|
42
|
+
faraday_env.status,
|
43
|
+
faraday_env.response_headers,
|
44
|
+
[faraday_env.body]
|
45
|
+
]
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -1,16 +1,17 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
class TrafficLogger
|
3
5
|
class Formatter
|
4
|
-
class Stream <
|
6
|
+
class Stream < Formatter
|
5
7
|
|
6
8
|
def initialize(**options)
|
7
9
|
@simulator = StreamSimulator.new(**options)
|
8
10
|
end
|
9
11
|
|
10
12
|
def format(hash)
|
11
|
-
time = hash[
|
12
|
-
|
13
|
-
"@ #{time.strftime '%a %d %b \'%y %T'}.#{'%d' % (time.usec / 1e4)} ##{id}\n#{@simulator.format(hash)}\n\n"
|
13
|
+
time = Time.parse(hash['timestamp'])
|
14
|
+
"@ #{time.strftime '%a %d %b \'%y %T.%3N'} ##{hash['request_log_id']}\n#{@simulator.format(hash)}\n\n"
|
14
15
|
end
|
15
16
|
|
16
17
|
end
|
@@ -35,9 +35,6 @@ module Rack
|
|
35
35
|
def writeline(line)
|
36
36
|
begin
|
37
37
|
hash = JSON.parse(line)
|
38
|
-
hash[:timestamp] = Time.parse(hash['timestamp'])
|
39
|
-
hash[:request_log_id] = hash['request_log_id']
|
40
|
-
hash[:event] = hash['event']
|
41
38
|
@output << @formatter.format(hash)
|
42
39
|
rescue
|
43
40
|
@output << line
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Rack
|
2
|
+
class TrafficLogger
|
3
|
+
class Request
|
4
|
+
|
5
|
+
def initialize(logger)
|
6
|
+
@logger = logger
|
7
|
+
@id = SecureRandom.hex 4
|
8
|
+
@started_at = Time.now
|
9
|
+
end
|
10
|
+
|
11
|
+
def start(env)
|
12
|
+
@verb = env['REQUEST_METHOD']
|
13
|
+
@env = env
|
14
|
+
end
|
15
|
+
|
16
|
+
def finish(response)
|
17
|
+
@code = Array === response ? response.first.to_i : 0
|
18
|
+
@options = @logger.options.for(@verb, @code)
|
19
|
+
if @options.basic?
|
20
|
+
log_request @env
|
21
|
+
log_response @env, response if @code > 0
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
BASIC_AUTH_PATTERN = /^basic ([a-z\d+\/]+={0,2})$/i
|
28
|
+
|
29
|
+
def log_request(env)
|
30
|
+
log 'request' do |hash|
|
31
|
+
if @options.request_headers?
|
32
|
+
hash.merge! env.reject { |_, v| v.respond_to? :read }
|
33
|
+
else
|
34
|
+
hash.merge! env.select { |k, _| BASIC_ENV_PROPERTIES.include? k }
|
35
|
+
end
|
36
|
+
|
37
|
+
hash['BASIC_AUTH_USERINFO'] = $1.unpack('m').first.split(':', 2) if hash['HTTP_AUTHORIZATION'] =~ BASIC_AUTH_PATTERN
|
38
|
+
|
39
|
+
input = env['rack.input']
|
40
|
+
if input && @options.request_bodies?
|
41
|
+
add_body_to_hash input.tap(&:rewind).read, env['CONTENT_ENCODING'] || env['HTTP_CONTENT_ENCODING'], hash
|
42
|
+
input.rewind
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def log_response(env, response)
|
48
|
+
code, headers, body = response
|
49
|
+
code = code.to_i
|
50
|
+
headers = HeaderHash.new(headers) if @options.response_headers? || @options.response_bodies?
|
51
|
+
log 'response' do |hash|
|
52
|
+
hash['http_version'] = env['HTTP_VERSION'] || 'HTTP/1.1'
|
53
|
+
hash['status_code'] = code
|
54
|
+
hash['status_name'] = Utils::HTTP_STATUS_CODES[code]
|
55
|
+
hash['headers'] = headers if @options.response_headers?
|
56
|
+
add_body_to_hash get_real_body(body), headers['Content-Encoding'], hash if @options.response_bodies?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Rack allows response bodies to be a few different things. This method
|
61
|
+
# ensures we get a string back.
|
62
|
+
def get_real_body(body)
|
63
|
+
|
64
|
+
# For bodies representing temporary files
|
65
|
+
body = File.open(body.path, 'rb') { |f| f.read } if body.respond_to? :path
|
66
|
+
|
67
|
+
# For bodies representing streams
|
68
|
+
body = body.read.tap { body.rewind } if body.respond_to? :read
|
69
|
+
|
70
|
+
# When body is an array (the common scenario)
|
71
|
+
body = body.join if body.respond_to? :join
|
72
|
+
|
73
|
+
# When body is a proxy
|
74
|
+
body = body.body while Rack::BodyProxy === body
|
75
|
+
|
76
|
+
# It should be a string now. Just in case it's not...
|
77
|
+
body.to_s
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
def add_body_to_hash(body, encoding, hash)
|
82
|
+
body = Zlib::GzipReader.new(StringIO.new body).read if encoding == 'gzip'
|
83
|
+
body.force_encoding 'UTF-8'
|
84
|
+
if body.valid_encoding?
|
85
|
+
hash['body'] = body
|
86
|
+
else
|
87
|
+
hash['body_base64'] = [body].pack 'm0'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def log(event)
|
92
|
+
# noinspection RubyStringKeysInHashInspection
|
93
|
+
hash = {
|
94
|
+
'timestamp' => Time.now.strftime('%FT%T.%3N%:z'),
|
95
|
+
'request_log_id' => @id,
|
96
|
+
'event' => event
|
97
|
+
}
|
98
|
+
yield hash rescue hash.merge! error: $!
|
99
|
+
@logger.log hash
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -8,7 +8,7 @@ module Rack
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def format(input)
|
11
|
-
case input[
|
11
|
+
case input['event']
|
12
12
|
when 'request' then format_request input
|
13
13
|
when 'response' then format_response input
|
14
14
|
else nil
|
@@ -38,7 +38,7 @@ module Rack
|
|
38
38
|
result = render REQUEST_TEMPLATES[@color],
|
39
39
|
verb: input['REQUEST_METHOD'],
|
40
40
|
path: input['PATH_INFO'],
|
41
|
-
qs: (q = input['QUERY_STRING']).empty? ? '' : "?#{q}",
|
41
|
+
qs: (q = input['QUERY_STRING'] || '').empty? ? '' : "?#{q}",
|
42
42
|
http: input['HTTP_VERSION'] || 'HTTP/1.1'
|
43
43
|
result << format_headers(env_request_headers input)
|
44
44
|
result << format_body(input, input['CONTENT_TYPE'] || input['HTTP_CONTENT_TYPE'])
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-traffic-logger
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Neil E. Pearson
|
@@ -38,6 +38,7 @@ files:
|
|
38
38
|
- lib/rack/traffic_logger.rb
|
39
39
|
- lib/rack/traffic_logger/echo.rb
|
40
40
|
- lib/rack/traffic_logger/express_setup.rb
|
41
|
+
- lib/rack/traffic_logger/faraday_adapter.rb
|
41
42
|
- lib/rack/traffic_logger/formatter.rb
|
42
43
|
- lib/rack/traffic_logger/formatter/json.rb
|
43
44
|
- lib/rack/traffic_logger/formatter/stream.rb
|
@@ -45,6 +46,7 @@ files:
|
|
45
46
|
- lib/rack/traffic_logger/option_interpreter.rb
|
46
47
|
- lib/rack/traffic_logger/option_interpreter/shorthand.rb
|
47
48
|
- lib/rack/traffic_logger/reader.rb
|
49
|
+
- lib/rack/traffic_logger/request.rb
|
48
50
|
- lib/rack/traffic_logger/stream_simulator.rb
|
49
51
|
- lib/rack/traffic_logger/version.rb
|
50
52
|
homepage: https://github.com/hx/rack-traffic-logger
|