rack-traffic-logger 0.1.4 → 0.2.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/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
|