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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6a455d4887387282d5194961bad8442b7f05d1cd
4
- data.tar.gz: 6bfc39d711eebf00a9c93f6e2035f70ea524babe
3
+ metadata.gz: f50ccbadf767b049731815d271ce316d986d916a
4
+ data.tar.gz: a6a970b4368ff2ed2a88a3ab66384dc32e046220
5
5
  SHA512:
6
- metadata.gz: 489b81dcb76375c24c0bdc8d9e933494c7134d4264aa3ef83e6176176ae6d32768b7f1ff226323404198dfe51a0612b52d3aa0cf6e951bf35f059dcb48e7d1ff
7
- data.tar.gz: c51a533ccbc5868fc9a7f44cb0f7f6355d362eb51aa9cc1badd70011871adcd9ab328ef6b09a840676dfeb53c1a944983e64e6ae5f7d02f07ce6f0d3e51f4302
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', response_bodies: false, colors: true
20
+ use Rack::TrafficLogger, 'path/to/file.log'
21
21
  ```
22
22
 
23
- - If you don't provide a log, everything will go to `STDOUT`.
24
- - You can supply either the path to a log file, an open file handle, or an instance of `Logger`.
25
- - See [this file](https://github.com/hx/rack-traffic-logger/blob/develop/lib/rack/traffic_logger.rb) for a list of other options that can affect logging style/filtering.
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(env['rack.input'].tap(&:rewind).read).to_json
10
- headers = {'Content-Type' => 'application/json'}
11
- if env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/
12
- zipped = StringIO.new('w')
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,14 @@
1
+ require_relative 'formatter/stream'
2
+ require_relative 'formatter/json'
3
+
4
+ module Rack
5
+ class TrafficLogger
6
+ class Formatter
7
+
8
+ def format(hash)
9
+ raise NotImplementedError
10
+ end
11
+
12
+ end
13
+ end
14
+ 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
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  class TrafficLogger
3
- VERSION = '0.1.4'
3
+ VERSION = '0.2.0'
4
4
  end
5
5
  end
@@ -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
- def initialize(app, logger = nil, options = {})
36
- Raise "Expected a Hash, but got #{options.class.name}" unless Hash === options
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
- case logger
39
- when nil, false then logger = Logger.new(STDOUT)
40
- when String, IO then logger = Logger.new(logger)
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
- env.delete 'HTTP_ACCEPT_ENCODING' if prevent_compression
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
- private
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 self.default_options
60
- @default_options ||= PUBLIC_ATTRIBUTES.map { |k, v| [k, v[:default]] }.to_h
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
- def render(template, data)
64
- template.gsub(/:(\w+)/) { data[$1.to_sym] }
65
- end
51
+ private
66
52
 
67
- REQUEST_TEMPLATES = {
68
- true => "\e[35m:verb \e[36m:path:qs\e[0m :http",
69
- false => ':verb :path:qs :http'
70
- }
71
-
72
- def log_request!(env)
73
- debug render REQUEST_TEMPLATES[colors],
74
- verb: env['REQUEST_METHOD'],
75
- path: env['PATH_INFO'],
76
- qs: (q = env['QUERY_STRING']).empty? ? '' : "?#{q}",
77
- http: env['HTTP_VERSION'] || 'HTTP/1.1'
78
- if request_headers || request_bodies
79
- headers = HeaderHash.new(env_request_headers env)
80
- log_headers! headers if request_headers
81
- input = env['rack.input']
82
- if request_bodies && input
83
- log_body! input.read,
84
- type: headers['Content-Type'],
85
- encoding: headers['Content-Encoding']
86
- input.rewind
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
- RESPONSE_TEMPLATES = {
92
- true => ":http \e[:color:code \e[36m:status\e[0m",
93
- false => ':http :code :status'
94
- }
76
+ private
95
77
 
96
- def status_color(status)
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
- def log_response!(env, response)
105
- debug render RESPONSE_TEMPLATES[colors],
106
- http: env['HTTP_VERSION'] || 'HTTP/1.1',
107
- code: code = response[0],
108
- status: Rack::Utils::HTTP_STATUS_CODES[code],
109
- color: status_color(code)
110
- if response_headers || response_bodies
111
- headers = HeaderHash.new(response[1])
112
- log_headers! headers if response_headers
113
- if response_bodies
114
- body = response[2]
115
- body = ::File.open(body.path, 'rb') { |f| f.read } if body.respond_to? :path
116
- if body.respond_to? :read
117
- stream = body
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
- def log_body!(body, type: nil, encoding: nil)
131
- return unless body
132
- body = Zlib::GzipReader.new(StringIO.new body).read if encoding == 'gzip'
133
- body = JSON.pretty_generate(JSON.parse body) if type[/[^;]+/] == 'application/json' && pretty_print
134
- body = "<#BINARY #{body.bytes.length} bytes>" if body =~ /[^[:print:]\r\n\t]/
135
- info body
136
- end
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
- HEADER_TEMPLATES = {
139
- true => "\e[4m:key\e[0m: :val\n",
140
- false => ":key: :val\n"
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
- def log_headers!(headers)
144
- info headers.map { |k, v| render HEADER_TEMPLATES[colors], key: k, val: v }.join
145
- end
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.1.4
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-01 00:00:00.000000000 Z
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/logger.rb
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