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 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