1 # -*- encoding: binary -*-
4 # Not at all optimized for performance, this was written based on
5 # the original C extension code so it's not very Ruby-ish...
8 attr_accessor :env, :status, :headers, :body
9 attr_writer :body_bytes_sent
11 def initialize(app, opts = {})
12 # trigger autoload to avoid thread-safety issues later on
13 Rack::Utils::HeaderHash
16 @logger = opts[:logger]
19 raise ArgumentError, ":logger and :path are independent"
20 path and @logger = File.open(path, "ab")
22 @logger.sync = true if @logger.respond_to?(:sync=)
23 @fmt_ops = compile_format(opts[:format] || Format::Common, opts)
24 @wrap_body = need_wrap_body?(@fmt_ops)
25 @reentrant = opts[:reentrant]
26 @need_resp = need_response_headers?(@fmt_ops)
33 unless resp.instance_of?(Array) && resp.size == 3
35 raise TypeError, "app response not a 3 element Array: #{resp.inspect}"
37 status, headers, body = resp
38 headers = Rack::Utils::HeaderHash.new(headers) if @need_resp
40 @reentrant = env['rack.multithread'] if @reentrant.nil?
41 wbody = @reentrant ? self.dup : self
44 wbody.headers = headers
46 return [ status, headers, wbody ]
48 log(env, status, headers)
49 [ status, headers, body ]
55 @body_bytes_sent += Rack::Utils.bytesize(part)
62 @body.close if @body.respond_to?(:close)
64 log(@env, @status, @headers)
76 @logger.respond_to?(:fileno) ? @logger.fileno : nil
80 :close == m.to_sym || @body.respond_to?(m)
85 # try to avoid unnecessary path lookups with to_io.stat instead of
88 @body.respond_to?(:to_io) ? @body.to_io.stat.size : File.size(rv)
100 s.force_encoding(Encoding::BINARY) if defined?(Encoding::BINARY)
101 s.gsub!(/(['"\x00-\x1f\x7f-\xff])/) do |x|
102 "\\x#{$1.unpack('H2').first.upcase}"
107 SPECIAL_RMAP = SPECIAL_VARS.inject([]) { |ary, (k,v)| ary[v] = k; ary }
110 ru = env['REQUEST_URI'] and return byte_xs(ru)
111 qs = env['QUERY_STRING']
112 qs.empty? or qs = "?#{byte_xs(qs)}"
113 "#{byte_xs(env['PATH_INFO'])}#{qs}"
116 def special_var(special_nr, env, status, headers)
117 case SPECIAL_RMAP[special_nr]
118 when :body_bytes_sent
119 @body_bytes_sent.to_s
122 status >= 100 && status <= 999 ? ('%03d' % status) : '-'
124 version = env['HTTP_VERSION'] and version = " #{byte_xs(version)}"
125 qs = env['QUERY_STRING']
126 qs.empty? or qs = "?#{byte_xs(qs)}"
127 "#{env['REQUEST_METHOD']} " \
128 "#{request_uri(env)}#{version}"
132 env['rack.input'].size.to_s
133 when :response_length
134 @body_bytes_sent == 0 ? '-' : @body_bytes_sent.to_s
136 xff = env['HTTP_X_FORWARDED_FOR'] and return byte_xs(xff)
137 env['REMOTE_ADDR'] || '-'
145 sign = off < 0 ? '-' : '+'
146 sprintf("%02d/%s/%d:%02d:%02d:%02d #{sign}%02d%02d",
147 t.mday, Time::RFC2822_MONTH_NAME[t.mon - 1],
148 t.year, t.hour, t.min, t.sec, *(off.abs / 60).divmod(60))
151 sprintf("%02d/%s/%d:%02d:%02d:%02d +0000",
152 t.mday, Time::RFC2822_MONTH_NAME[t.mon - 1],
153 t.year, t.hour, t.min, t.sec)
155 raise "EDOOFUS #{special_nr}"
159 def time_format(sec, usec, format, div)
160 format % [ sec, usec / div ]
163 def log(env, status, headers)
164 (@logger || env['rack.errors']) << @fmt_ops.map { |op|
166 when OP_LITERAL; op[1]
167 when OP_REQUEST; byte_xs(env[op[1]] || "-")
168 when OP_RESPONSE; byte_xs(headers[op[1]] || "-")
169 when OP_SPECIAL; special_var(op[1], env, status, headers)
170 when OP_EVAL; eval(op[1]).to_s rescue "-"
171 when OP_TIME_LOCAL; Time.now.strftime(op[1])
172 when OP_TIME_UTC; Time.now.utc.strftime(op[1])
174 t = Time.now - @start
175 time_format(t.to_i, (t - t.to_i) * 1000000, op[1], op[2])
178 time_format(t.to_i, t.usec, op[1], op[2])
180 (env['rack.request.cookie_hash'][op[1]] rescue "-") || "-"
182 raise "EDOOFUS #{op.inspect}"