rack-traffic-logger 0.1.4 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +114 -4
- data/lib/rack/traffic_logger/echo.rb +15 -17
- data/lib/rack/traffic_logger/formatter/json.rb +18 -0
- data/lib/rack/traffic_logger/formatter/stream.rb +19 -0
- data/lib/rack/traffic_logger/formatter.rb +14 -0
- data/lib/rack/traffic_logger/option_interpreter.rb +149 -0
- data/lib/rack/traffic_logger/stream_simulator.rb +87 -0
- data/lib/rack/traffic_logger/version.rb +1 -1
- data/lib/rack/traffic_logger.rb +120 -117
- metadata +7 -3
- data/lib/rack/traffic_logger/logger.rb +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f50ccbadf767b049731815d271ce316d986d916a
|
4
|
+
data.tar.gz: a6a970b4368ff2ed2a88a3ab66384dc32e046220
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 29df161ebf86d8f6735f92f8d85314450c53534ef9e6fbcc0c67edc0f306b014bea4cd5920a41e520e9f86698feb85d5b1bf5d948764cb8bd64e4c73d00b3a11
|
7
|
+
data.tar.gz: 8331c0187a9d47ecbda6972f18724801dc6b3b806f32f5c1a95af3f220320eb71d79db85c9fbfb7047652ee2f1d0345d73ebc3762816e609d483d17bc822de24
|
data/README.md
CHANGED
@@ -17,9 +17,119 @@ require 'rack/traffic_logger'
|
|
17
17
|
Then, in your `config.ru` or wherever you set up your middleware stack:
|
18
18
|
|
19
19
|
```ruby
|
20
|
-
use Rack::TrafficLogger, 'path/to/file.log'
|
20
|
+
use Rack::TrafficLogger, 'path/to/file.log'
|
21
21
|
```
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
By default, simple stream-like output will be written:
|
24
|
+
|
25
|
+
```
|
26
|
+
@ Wed 12 Nov '14 15:19:48.0 #48f8ed62
|
27
|
+
GET /home HTTP/1.1
|
28
|
+
|
29
|
+
@ Wed 12 Nov '14 15:19:48.0 #48f8ed62
|
30
|
+
HTTP/1.1 200 OK
|
31
|
+
```
|
32
|
+
|
33
|
+
The part after the `#` is the **request log ID**, which is unique to each request. It lets you match up a response with its request, in case you have multiple listeners.
|
34
|
+
|
35
|
+
You can add some colour, and JSON request/response body pretty-printing, by specifying a formatter:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
use Rack::TrafficLogger, 'file.log', Rack::TrafficLogger::Formatter::Stream.new(color: true, pretty_print: true)
|
39
|
+
```
|
40
|
+
|
41
|
+
You can also output JSON (great for sending to log analyzers like Splunk):
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
use Rack::TrafficLogger, 'file.log', Rack::TrafficLogger::Formatter::JSON.new
|
45
|
+
```
|
46
|
+
|
47
|
+
### Filtering
|
48
|
+
|
49
|
+
By default, only basic request/response details are logged:
|
50
|
+
|
51
|
+
```json
|
52
|
+
{
|
53
|
+
"timestamp": "2014-11-12 15:30:20 +1100",
|
54
|
+
"request_log_id": "d67e5591",
|
55
|
+
"event": "request",
|
56
|
+
"SERVER_NAME": "localhost",
|
57
|
+
"REQUEST_METHOD": "GET",
|
58
|
+
"PATH_INFO": "/home",
|
59
|
+
"HTTP_VERSION": "HTTP/1.1",
|
60
|
+
"SERVER_PORT": "80",
|
61
|
+
"QUERY_STRING": "",
|
62
|
+
"REMOTE_ADDR": "127.0.0.1"
|
63
|
+
}
|
64
|
+
{
|
65
|
+
"timestamp": "2014-11-12 15:30:20 +1100",
|
66
|
+
"request_log_id": "d67e5591",
|
67
|
+
"event": "response",
|
68
|
+
"http_version": "HTTP/1.1",
|
69
|
+
"status_code": 200,
|
70
|
+
"status_name": "OK"
|
71
|
+
}
|
72
|
+
```
|
73
|
+
|
74
|
+
You can specify other parts of requests/responses to be logged:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
use Rack::TrafficLogger, 'file.log', :request_headers, :response_bodies
|
78
|
+
```
|
79
|
+
|
80
|
+
Optional sections include `request_headers`, `request_bodies`, `response_headers`, and `response_bodies`. You can also specify `headers` for both request and response headers, and `bodies` for both request and response bodies. Or, specify `all` if you want the lot. Combine tokens to get the output you need:
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
use Rack::TrafficLogger, 'file.log', :headers, :response_bodies # Everything except request bodies!
|
84
|
+
# Or:
|
85
|
+
use Rack::TrafficLogger, 'file.log', :all # Everything
|
86
|
+
```
|
87
|
+
|
88
|
+
If you want to use a custom formatter, make sure you include it before any filtering arguments:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
use Rack::TrafficLogger, 'file.log', Rack::TrafficLogger::Formatter::JSON.new, :headers
|
92
|
+
```
|
93
|
+
|
94
|
+
You can specify that you want different parts logged based on the kind of request that was made:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
use Rack::TrafficLogger, 'file.log', :headers, post: :request_bodies # Log headers for all requests, and also request bodies for POST requests
|
98
|
+
```
|
99
|
+
|
100
|
+
You can also exclude other request verbs entirely:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
use Rack::TrafficLogger, 'file.log', only: {post: [:headers, :request_bodies]} # Log only POST requests, and include all headers, and request bodies
|
104
|
+
```
|
105
|
+
|
106
|
+
This can be shortened to:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
use Rack::TrafficLogger, 'file.log', :post, :headers, :request_bodies
|
110
|
+
```
|
111
|
+
|
112
|
+
Or if you only want the basics of POST requests, without headers/bodies:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
use Rack::TrafficLogger, 'file.log', :post
|
116
|
+
```
|
117
|
+
|
118
|
+
You can apply the same filtration based on response status codes:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
use Rack::TrafficLogger, 'file.log', 404 # Only log requests that are not-found
|
122
|
+
```
|
123
|
+
|
124
|
+
Include as many as you like, and even use ranges:
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
use Rack::TrafficLogger, 'file.log', 301, 302, 400...600 # Log redirects and errors
|
128
|
+
```
|
129
|
+
|
130
|
+
If you need to, you can get pretty fancy:
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
use Rack::TrafficLogger, 'file.log', :request_headers, 401 => false, 500...600 => :all, 200...300 => {post: :request_bodies, delete: false}
|
134
|
+
use Rack::TrafficLogger, 'file.log', [:get, :head] => 200..204, post: {only: {201 => :request_bodies}}, [:put, :patch] => :all
|
135
|
+
```
|
@@ -5,25 +5,23 @@ module Rack
|
|
5
5
|
class TrafficLogger
|
6
6
|
class Echo
|
7
7
|
def call(env)
|
8
|
+
body = env['rack.input'].tap(&:rewind).read
|
9
|
+
headers = {}
|
8
10
|
begin
|
9
|
-
body = JSON.parse(
|
10
|
-
headers
|
11
|
-
|
12
|
-
|
13
|
-
writer = Zlib::GzipWriter.new(zipped)
|
14
|
-
writer.write body
|
15
|
-
writer.close
|
16
|
-
body = zipped.string
|
17
|
-
headers['Content-Encoding'] = 'gzip'
|
18
|
-
end
|
19
|
-
[200, headers, [body]]
|
20
|
-
rescue JSON::ParserError => error
|
21
|
-
[
|
22
|
-
500,
|
23
|
-
{'Content-Type' => 'text/plain;charset=UTF-8'},
|
24
|
-
[error.message]
|
25
|
-
]
|
11
|
+
body = JSON.parse(body).to_json
|
12
|
+
headers['Content-Type'] = 'application/json'
|
13
|
+
rescue JSON::ParserError
|
14
|
+
# ignored
|
26
15
|
end
|
16
|
+
if env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/
|
17
|
+
zipped = StringIO.new('w')
|
18
|
+
writer = Zlib::GzipWriter.new(zipped)
|
19
|
+
writer.write body
|
20
|
+
writer.close
|
21
|
+
body = zipped.string
|
22
|
+
headers['Content-Encoding'] = 'gzip'
|
23
|
+
end
|
24
|
+
[200, headers, [body]]
|
27
25
|
end
|
28
26
|
end
|
29
27
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class TrafficLogger
|
5
|
+
class Formatter
|
6
|
+
class JSON < self
|
7
|
+
|
8
|
+
def initialize(pretty_print: false)
|
9
|
+
formatter = pretty_print ?
|
10
|
+
-> hash { ::JSON.pretty_generate(hash) << "\n" } :
|
11
|
+
-> hash { ::JSON.generate(hash) << "\n" }
|
12
|
+
define_singleton_method :format, formatter
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Rack
|
2
|
+
class TrafficLogger
|
3
|
+
class Formatter
|
4
|
+
class Stream < self
|
5
|
+
|
6
|
+
def initialize(**options)
|
7
|
+
@simulator = StreamSimulator.new(**options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def format(hash)
|
11
|
+
time = hash[:timestamp]
|
12
|
+
id = hash[:request_log_id]
|
13
|
+
"@ #{time.strftime '%a %d %b \'%y %T'}.#{'%d' % (time.usec / 1e4)} ##{id}\n#{@simulator.format(hash)}\n\n"
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module Rack
|
2
|
+
class TrafficLogger
|
3
|
+
class OptionInterpreter
|
4
|
+
|
5
|
+
VERBS = %i[get post put patch delete head options trace]
|
6
|
+
TYPES = %i[request_headers response_headers request_bodies response_bodies]
|
7
|
+
|
8
|
+
def initialize(*options)
|
9
|
+
@tests = {}
|
10
|
+
add_rules options
|
11
|
+
end
|
12
|
+
|
13
|
+
class Rule
|
14
|
+
attr_reader :arg, :filter
|
15
|
+
def initialize(arg, filter = {})
|
16
|
+
@arg = arg
|
17
|
+
@filter = filter
|
18
|
+
end
|
19
|
+
def inspect
|
20
|
+
"<#{filter} #{self.class.name[/[^:]+$/]}: #{arg}>"
|
21
|
+
end
|
22
|
+
def applies?(verb, code)
|
23
|
+
return false if filter[:verb] && verb != filter[:verb]
|
24
|
+
!filter[:code] || filter[:code] === code
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class OnlyVerb < Rule; end
|
29
|
+
class OnlyCode < Rule; end
|
30
|
+
class Include < Rule; end
|
31
|
+
class Exclude < Rule; end
|
32
|
+
|
33
|
+
TYPES.each do |type|
|
34
|
+
define_method(:"#{type}?") { |verb, code| test verb, code, type }
|
35
|
+
end
|
36
|
+
|
37
|
+
class OptionProxy
|
38
|
+
def initialize(interpreter, verb, code)
|
39
|
+
@interpreter = interpreter
|
40
|
+
@verb = verb
|
41
|
+
@code = code
|
42
|
+
end
|
43
|
+
(TYPES + [:basic]).each do |type|
|
44
|
+
method = :"#{type}?"
|
45
|
+
define_method(method) { @interpreter.__send__ method, @verb, @code }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def for(verb, code)
|
50
|
+
OptionProxy.new self, verb, code
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_rules(input, **filter)
|
54
|
+
input = [input] unless Array === input
|
55
|
+
verb = filter[:verb]
|
56
|
+
code = filter[:code]
|
57
|
+
input.each do |token|
|
58
|
+
case token
|
59
|
+
when *VERBS
|
60
|
+
raise "Verb on verb (#{token} on #{verb})" if verb
|
61
|
+
rules << OnlyVerb.new(token, filter)
|
62
|
+
when Fixnum, Range
|
63
|
+
raise "Code on code (#{token} on #{code})" if code
|
64
|
+
rules << OnlyCode.new(token, filter)
|
65
|
+
when *TYPES
|
66
|
+
rules << Include.new(token, filter)
|
67
|
+
when FalseClass
|
68
|
+
rules << Exclude.new(nil, filter)
|
69
|
+
when :headers then add_rules [:request_headers, :response_headers], **filter
|
70
|
+
when :bodies then add_rules [:request_bodies, :response_bodies], **filter
|
71
|
+
when :all then add_rules [:headers, :bodies], **filter
|
72
|
+
when Hash
|
73
|
+
if token.keys == [:only]
|
74
|
+
inner_hash = token.values.first
|
75
|
+
raise 'You can only use :only => {} with a Hash' unless Hash === inner_hash
|
76
|
+
add_rules inner_hash.keys, **filter
|
77
|
+
add_rule_hash inner_hash, **filter
|
78
|
+
else
|
79
|
+
add_rule_hash token, **filter
|
80
|
+
end
|
81
|
+
else raise "Invalid token of type #{token.class.name} : #{token}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def add_rule_hash(hash, **filter)
|
87
|
+
hash.each { |k, v| add_rule_pair k, v, **filter }
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_rule_pair(name, value, **filter)
|
91
|
+
case name
|
92
|
+
when *VERBS then add_rules value, **filter.merge(verb: name)
|
93
|
+
when Fixnum, Range then add_rules value, **filter.merge(code: name)
|
94
|
+
when Array then name.each { |n| add_rule_pair n, value, **filter }
|
95
|
+
else raise "Invalid token of type #{name.class.name} : #{name}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Test whether a given verb, status code, and log type should be logged
|
100
|
+
# @param verb [Symbol] One of the {self::VERBS} symbols
|
101
|
+
# @param code [Fixnum] The HTTP status code
|
102
|
+
# @param type [Symbol|NilClass] One of the {self::TYPES} symbols, or `nil` for basic request/response details
|
103
|
+
# @return [TrueClass|FalseClass] Whether the type should be logged
|
104
|
+
def test(*args)
|
105
|
+
if @tests.key? args
|
106
|
+
@tests[args]
|
107
|
+
else
|
108
|
+
@tests[args] = _test *args
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
alias :basic? :test
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def _test(verb, code, type = nil)
|
117
|
+
|
118
|
+
# To start, only allow if not a header/body
|
119
|
+
type_result = type == nil
|
120
|
+
|
121
|
+
# Exclusivity filters
|
122
|
+
only_code = nil
|
123
|
+
only_verb = nil
|
124
|
+
|
125
|
+
# Loop through rules that apply to this verb and code
|
126
|
+
rules.select { |r| r.applies? verb, code }.each do |rule|
|
127
|
+
case rule
|
128
|
+
when Include then type_result ||= rule.arg == type
|
129
|
+
when OnlyVerb then only_verb ||= rule.arg == verb
|
130
|
+
when OnlyCode then only_code ||= rule.arg === code
|
131
|
+
when Exclude
|
132
|
+
only_verb ||= false if rule.filter[:verb]
|
133
|
+
only_code ||= false if rule.filter[:code]
|
134
|
+
else nil
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Pass if the type was accepted, and exclusivity filters passed
|
139
|
+
type_result && ![only_verb, only_code].include?(false)
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
def rules
|
144
|
+
@rules ||= []
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Rack
|
2
|
+
class TrafficLogger
|
3
|
+
class StreamSimulator
|
4
|
+
|
5
|
+
def initialize(color: false, pretty_print: false)
|
6
|
+
@color = color
|
7
|
+
@pretty_print = pretty_print
|
8
|
+
end
|
9
|
+
|
10
|
+
def format(input)
|
11
|
+
case input[:event]
|
12
|
+
when 'request' then format_request input
|
13
|
+
when 'response' then format_response input
|
14
|
+
else nil
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
REQUEST_TEMPLATES = {
|
21
|
+
true => "\e[35m:verb \e[36m:path:qs\e[0m :http",
|
22
|
+
false => ':verb :path:qs :http'
|
23
|
+
}
|
24
|
+
|
25
|
+
RESPONSE_TEMPLATES = {
|
26
|
+
true => ":http \e[:color:code \e[36m:status\e[0m",
|
27
|
+
false => ':http :code :status'
|
28
|
+
}
|
29
|
+
|
30
|
+
HEADER_TEMPLATES = {
|
31
|
+
true => "\n\e[4m:key\e[0m: :val\e[34m:extra\e[0m",
|
32
|
+
false => "\n:key: :val:extra"
|
33
|
+
}
|
34
|
+
|
35
|
+
BASIC_AUTH_PATTERN = /^basic ([a-z\d+\/]+={0,2})$/i
|
36
|
+
|
37
|
+
def format_request(input)
|
38
|
+
result = render REQUEST_TEMPLATES[@color],
|
39
|
+
verb: input['REQUEST_METHOD'],
|
40
|
+
path: input['PATH_INFO'],
|
41
|
+
qs: (q = input['QUERY_STRING']).empty? ? '' : "?#{q}",
|
42
|
+
http: input['HTTP_VERSION'] || 'HTTP/1.1'
|
43
|
+
result << format_headers(env_request_headers input)
|
44
|
+
result << format_body(input, input['CONTENT_TYPE'] || input['HTTP_CONTENT_TYPE'])
|
45
|
+
end
|
46
|
+
|
47
|
+
def format_response(input)
|
48
|
+
result = render RESPONSE_TEMPLATES[@color],
|
49
|
+
http: input['http_version'] || 'HTTP/1.1',
|
50
|
+
code: input['status_code'],
|
51
|
+
status: input['status_name']
|
52
|
+
headers = input['headers']
|
53
|
+
result << format_headers(headers) if headers
|
54
|
+
result << format_body(input, headers && headers['Content-Type'])
|
55
|
+
end
|
56
|
+
|
57
|
+
def render(template, data)
|
58
|
+
template.gsub(/:(\w+)/) { data[$1.to_sym] }
|
59
|
+
end
|
60
|
+
|
61
|
+
def format_headers(headers)
|
62
|
+
headers = HeaderHash.new(headers) unless HeaderHash === headers
|
63
|
+
headers.map do |k, v|
|
64
|
+
data = {key: k, val: v}
|
65
|
+
data[:extra] = " #{$1.unpack('m').first}" if k == 'Authorization' && v =~ BASIC_AUTH_PATTERN
|
66
|
+
render HEADER_TEMPLATES[@color], data
|
67
|
+
end.join
|
68
|
+
end
|
69
|
+
|
70
|
+
def format_body(input, content_type)
|
71
|
+
body = input['body_base64'] ? input['body_base64'].unpack('m').first : input['body']
|
72
|
+
return '' unless body
|
73
|
+
body = JSON.pretty_generate(JSON.parse body) rescue body if @pretty_print && content_type =~ %r{^application/json(;|$)}
|
74
|
+
if body =~ /[^[:print:]\r\n\t]/
|
75
|
+
length = body.bytes.length
|
76
|
+
body = "<BINARY (#{length} byte#{length == 1 ? '' : 's'})>"
|
77
|
+
end
|
78
|
+
"\n\n#{body}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def env_request_headers(env)
|
82
|
+
env.select { |k, _| k =~ /^(CONTENT|HTTP)_(?!VERSION)/ }.map { |(k, v)| [k.sub(/^HTTP_/, ''), v] }.to_h
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/rack/traffic_logger.rb
CHANGED
@@ -1,151 +1,154 @@
|
|
1
1
|
require_relative 'traffic_logger/version'
|
2
|
-
require_relative 'traffic_logger/logger'
|
3
2
|
require_relative 'traffic_logger/header_hash'
|
3
|
+
require_relative 'traffic_logger/option_interpreter'
|
4
|
+
require_relative 'traffic_logger/stream_simulator'
|
5
|
+
require_relative 'traffic_logger/formatter'
|
4
6
|
|
5
|
-
require 'forwardable'
|
6
|
-
require 'rack/nulllogger'
|
7
7
|
require 'json'
|
8
|
+
require 'securerandom'
|
8
9
|
|
9
10
|
module Rack
|
10
11
|
class TrafficLogger
|
11
|
-
extend Forwardable
|
12
|
-
|
13
|
-
PUBLIC_ATTRIBUTES = {
|
14
|
-
request_headers: {type: [TrueClass, FalseClass], default: true},
|
15
|
-
request_bodies: {type: [TrueClass, FalseClass], default: true},
|
16
|
-
response_headers: {type: [TrueClass, FalseClass], default: true},
|
17
|
-
response_bodies: {type: [TrueClass, FalseClass], default: true},
|
18
|
-
colors: {type: [TrueClass, FalseClass], default: false},
|
19
|
-
prevent_compression: {type: [TrueClass, FalseClass], default: false},
|
20
|
-
pretty_print: {type: [TrueClass, FalseClass], default: false}
|
21
|
-
}
|
22
|
-
|
23
|
-
PUBLIC_ATTRIBUTES.each do |attr, props|
|
24
|
-
type = props[:type]
|
25
|
-
type = [type] unless Array === type
|
26
|
-
define_method(attr) { @options[attr] }
|
27
|
-
define_method :"#{attr}=" do |value|
|
28
|
-
raise "Expected one of [#{type.map(&:name).join ' | '}], got #{value.class.name}" unless type.find { |t| t === value }
|
29
|
-
@options[attr] = value
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
delegate %i(info debug warn error fatal) => :@logger
|
34
12
|
|
35
|
-
|
36
|
-
|
13
|
+
# These environment properties will always be logged as part of request logs
|
14
|
+
BASIC_ENV_PROPERTIES = %w[
|
15
|
+
REQUEST_METHOD
|
16
|
+
HTTPS
|
17
|
+
SERVER_NAME
|
18
|
+
SERVER_PORT
|
19
|
+
PATH_INFO
|
20
|
+
QUERY_STRING
|
21
|
+
HTTP_VERSION
|
22
|
+
REMOTE_HOST
|
23
|
+
REMOTE_ADDR
|
24
|
+
]
|
25
|
+
|
26
|
+
attr_reader :app, :options
|
27
|
+
|
28
|
+
def initialize(app, log_path, *options)
|
37
29
|
@app = app
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
else logger = Rack::NullLogger.new(nil) unless logger.respond_to? :debug
|
42
|
-
end
|
43
|
-
@logger = logger
|
44
|
-
@options = self.class.default_options.merge(options)
|
30
|
+
@log_path = log_path
|
31
|
+
@formatter = options.first.respond_to?(:format) ? options.shift : Formatter::Stream.new
|
32
|
+
@options = OptionInterpreter.new(*options)
|
45
33
|
end
|
46
34
|
|
47
35
|
def call(env)
|
48
|
-
|
49
|
-
safely('logging request') { log_request! env }
|
50
|
-
@app.call(env).tap { |response| safely('logging response') { log_response! env, response } }
|
36
|
+
Request.new(self).call env
|
51
37
|
end
|
52
38
|
|
53
|
-
|
54
|
-
|
55
|
-
def safely(action)
|
56
|
-
yield rescue error "Error #{action}: #{$!}"
|
39
|
+
def log(hash)
|
40
|
+
write @formatter.format hash
|
57
41
|
end
|
58
42
|
|
59
|
-
def
|
60
|
-
@
|
43
|
+
def write(data)
|
44
|
+
if @log_path.respond_to? :write
|
45
|
+
@log_path.write data
|
46
|
+
else
|
47
|
+
File.write @log_path, data, mode: 'a', encoding: data.encoding
|
48
|
+
end
|
61
49
|
end
|
62
50
|
|
63
|
-
|
64
|
-
template.gsub(/:(\w+)/) { data[$1.to_sym] }
|
65
|
-
end
|
51
|
+
private
|
66
52
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
53
|
+
class Request
|
54
|
+
|
55
|
+
def initialize(logger)
|
56
|
+
@logger = logger
|
57
|
+
@id = SecureRandom.hex 4
|
58
|
+
@started_at = Time.now
|
59
|
+
end
|
60
|
+
|
61
|
+
def call(env)
|
62
|
+
@verb = env['REQUEST_METHOD']
|
63
|
+
@env = env
|
64
|
+
begin
|
65
|
+
response = @logger.app.call(env)
|
66
|
+
ensure
|
67
|
+
@code = Array === response ? response.first.to_i : 0
|
68
|
+
@options = @logger.options.for(@verb, @code)
|
69
|
+
if @options.basic?
|
70
|
+
log_request env
|
71
|
+
log_response env, response if @code > 0
|
72
|
+
end
|
87
73
|
end
|
88
74
|
end
|
89
|
-
end
|
90
75
|
|
91
|
-
|
92
|
-
true => ":http \e[:color:code \e[36m:status\e[0m",
|
93
|
-
false => ':http :code :status'
|
94
|
-
}
|
76
|
+
private
|
95
77
|
|
96
|
-
|
97
|
-
case (status.to_i / 100).to_i
|
98
|
-
when 2 then '32m'
|
99
|
-
when 4, 5 then '31m'
|
100
|
-
else '33m'
|
101
|
-
end
|
102
|
-
end
|
78
|
+
BASIC_AUTH_PATTERN = /^basic ([a-z\d+\/]+={0,2})$/i
|
103
79
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
body = stream.tap(&:rewind).read
|
119
|
-
stream.rewind
|
80
|
+
def log_request(env)
|
81
|
+
log 'request' do |hash|
|
82
|
+
if @options.request_headers?
|
83
|
+
hash.merge! env.reject { |_, v| v.respond_to? :read }
|
84
|
+
else
|
85
|
+
hash.merge! env.select { |k, _| BASIC_ENV_PROPERTIES.include? k }
|
86
|
+
end
|
87
|
+
|
88
|
+
hash['BASIC_AUTH_USERINFO'] = $1.unpack('m').first.split(':', 2) if hash['HTTP_AUTHORIZATION'] =~ BASIC_AUTH_PATTERN
|
89
|
+
|
90
|
+
input = env['rack.input']
|
91
|
+
if input && @options.request_bodies?
|
92
|
+
add_body_to_hash input.tap(&:rewind).read, env['CONTENT_ENCODING'] || env['HTTP_CONTENT_ENCODING'], hash
|
93
|
+
input.rewind
|
120
94
|
end
|
121
|
-
body = body.join if body.respond_to? :join
|
122
|
-
body = body.body while Rack::BodyProxy === body
|
123
|
-
log_body! body,
|
124
|
-
type: headers['Content-Type'],
|
125
|
-
encoding: headers['Content-Encoding']
|
126
95
|
end
|
127
96
|
end
|
128
|
-
end
|
129
97
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
98
|
+
def log_response(env, response)
|
99
|
+
code, headers, body = response
|
100
|
+
code = code.to_i
|
101
|
+
headers = HeaderHash.new(headers) if @options.response_headers? || @options.response_bodies?
|
102
|
+
log 'response' do |hash|
|
103
|
+
hash['http_version'] = env['HTTP_VERSION'] || 'HTTP/1.1'
|
104
|
+
hash['status_code'] = code
|
105
|
+
hash['status_name'] = Utils::HTTP_STATUS_CODES[code]
|
106
|
+
hash['headers'] = headers if @options.response_headers?
|
107
|
+
add_body_to_hash get_real_body(body), headers['Content-Encoding'], hash if @options.response_bodies?
|
108
|
+
end
|
109
|
+
end
|
137
110
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
}
|
111
|
+
# Rack allows response bodies to be a few different things. This method
|
112
|
+
# ensures we get a string back.
|
113
|
+
def get_real_body(body)
|
142
114
|
|
143
|
-
|
144
|
-
|
145
|
-
|
115
|
+
# For bodies representing temporary files
|
116
|
+
body = File.open(body.path, 'rb') { |f| f.read } if body.respond_to? :path
|
117
|
+
|
118
|
+
# For bodies representing streams
|
119
|
+
body = body.read.tap { body.rewind } if body.respond_to? :read
|
120
|
+
|
121
|
+
# When body is an array (the common scenario)
|
122
|
+
body = body.join if body.respond_to? :join
|
123
|
+
|
124
|
+
# When body is a proxy
|
125
|
+
body = body.body while Rack::BodyProxy === body
|
126
|
+
|
127
|
+
# It should be a string now. Just in case it's not...
|
128
|
+
body.to_s
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
def add_body_to_hash(body, encoding, hash)
|
133
|
+
body = Zlib::GzipReader.new(StringIO.new body).read if encoding == 'gzip'
|
134
|
+
body.force_encoding 'UTF-8'
|
135
|
+
if body.valid_encoding?
|
136
|
+
hash['body'] = body
|
137
|
+
else
|
138
|
+
hash['body_base64'] = [body].pack 'm0'
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def log(event)
|
143
|
+
hash = {
|
144
|
+
timestamp: Time.now,
|
145
|
+
request_log_id: @id,
|
146
|
+
event: event
|
147
|
+
}
|
148
|
+
yield hash rescue hash.merge! error: $!
|
149
|
+
@logger.log hash
|
150
|
+
end
|
146
151
|
|
147
|
-
def env_request_headers(env)
|
148
|
-
env.select { |k, _| k =~ /^(CONTENT|HTTP)_(?!VERSION)/ }.map { |(k, v)| [k.sub(/^HTTP_/, ''), v] }.to_h
|
149
152
|
end
|
150
153
|
|
151
154
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-traffic-logger
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Neil E. Pearson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-11-
|
11
|
+
date: 2014-11-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -35,8 +35,12 @@ files:
|
|
35
35
|
- README.md
|
36
36
|
- lib/rack/traffic_logger.rb
|
37
37
|
- lib/rack/traffic_logger/echo.rb
|
38
|
+
- lib/rack/traffic_logger/formatter.rb
|
39
|
+
- lib/rack/traffic_logger/formatter/json.rb
|
40
|
+
- lib/rack/traffic_logger/formatter/stream.rb
|
38
41
|
- lib/rack/traffic_logger/header_hash.rb
|
39
|
-
- lib/rack/traffic_logger/
|
42
|
+
- lib/rack/traffic_logger/option_interpreter.rb
|
43
|
+
- lib/rack/traffic_logger/stream_simulator.rb
|
40
44
|
- lib/rack/traffic_logger/version.rb
|
41
45
|
homepage: https://github.com/hx/rack-traffic-logger
|
42
46
|
licenses:
|
@@ -1,25 +0,0 @@
|
|
1
|
-
require 'logger'
|
2
|
-
|
3
|
-
module Rack
|
4
|
-
class TrafficLogger
|
5
|
-
class Logger < ::Logger
|
6
|
-
|
7
|
-
def initialize(*args)
|
8
|
-
super *args
|
9
|
-
@default_formatter = Formatter.new
|
10
|
-
end
|
11
|
-
|
12
|
-
class Formatter
|
13
|
-
|
14
|
-
def call(severity, time, progname, msg)
|
15
|
-
if severity == 'INFO'
|
16
|
-
"#{msg}\n"
|
17
|
-
else
|
18
|
-
"@ #{time.strftime '%a %d %b \'%y %T'}.#{'%d' % (time.usec / 1e4)}\n#{msg}\n"
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|