avoid calling "<<" on env["rack.errors"]
[clogger.git] / lib / clogger / pure.rb
blob44f4e6276755cdcec17a3a96cb5356d5a1a8a862
1 # -*- encoding: binary -*-
2 # :stopdoc:
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...
6 class Clogger
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
15     @app = app
16     @logger = opts[:logger]
17     path = opts[:path]
18     path && @logger and
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)
27     @body_bytes_sent = 0
28   end
30   def call(env)
31     @start = Time.now
32     resp = @app.call(env)
33     unless resp.instance_of?(Array) && resp.size == 3
34       log(env, 500, {})
35       raise TypeError, "app response not a 3 element Array: #{resp.inspect}"
36     end
37     status, headers, body = resp
38     headers = Rack::Utils::HeaderHash.new(headers) if @need_resp
39     if @wrap_body
40       @reentrant = env['rack.multithread'] if @reentrant.nil?
41       wbody = @reentrant ? self.dup : self
42       wbody.env = env
43       wbody.status = status
44       wbody.headers = headers
45       wbody.body = body
46       return [ status, headers, wbody ]
47     end
48     log(env, status, headers)
49     [ status, headers, body ]
50   end
52   def each
53     @body_bytes_sent = 0
54     @body.each do |part|
55       @body_bytes_sent += Rack::Utils.bytesize(part)
56       yield part
57     end
58     self
59   end
61   def close
62     @body.close if @body.respond_to?(:close)
63     ensure
64       log(@env, @status, @headers)
65   end
67   def reentrant?
68     @reentrant
69   end
71   def wrap_body?
72     @wrap_body
73   end
75   def fileno
76     @logger.respond_to?(:fileno) ? @logger.fileno : nil
77   end
79   def respond_to?(m)
80     :close == m.to_sym || @body.respond_to?(m)
81   end
83   def to_path
84     rv = @body.to_path
85     # try to avoid unnecessary path lookups with to_io.stat instead of
86     # File.size
87     @body_bytes_sent =
88            @body.respond_to?(:to_io) ? @body.to_io.stat.size : File.size(rv)
89     rv
90   end
92   def to_io
93     @body.to_io
94   end
96 private
98   def byte_xs(s)
99     s = s.dup
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}"
103     end
104     s
105   end
107   SPECIAL_RMAP = SPECIAL_VARS.inject([]) { |ary, (k,v)| ary[v] = k; ary }
109   def request_uri(env)
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}"
114   end
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
120     when :status
121       status = status.to_i
122       status >= 100 && status <= 999 ? ('%03d' % status) : '-'
123     when :request
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}"
129     when :request_uri
130       request_uri(env)
131     when :request_length
132       env['rack.input'].size.to_s
133     when :response_length
134       @body_bytes_sent == 0 ? '-' : @body_bytes_sent.to_s
135     when :ip
136       xff = env['HTTP_X_FORWARDED_FOR'] and return byte_xs(xff)
137       env['REMOTE_ADDR'] || '-'
138     when :pid
139       $$.to_s
140     when :time_iso8601
141       Time.now.iso8601
142     when :time_local
143       t = Time.now
144       off = t.utc_offset
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))
149     when :time_utc
150       t = Time.now.utc
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)
154     else
155       raise "EDOOFUS #{special_nr}"
156     end
157   end
159   def time_format(sec, usec, format, div)
160     format % [ sec, usec / div ]
161   end
163   def log(env, status, headers)
164     str = @fmt_ops.map { |op|
165       case op[0]
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])
173       when OP_REQUEST_TIME
174         t = Time.now - @start
175         time_format(t.to_i, (t - t.to_i) * 1000000, op[1], op[2])
176       when OP_TIME
177         t = Time.now
178         time_format(t.to_i, t.usec, op[1], op[2])
179       when OP_COOKIE
180         (byte_xs(env['rack.request.cookie_hash'][op[1]]) rescue "-") || "-"
181       else
182         raise "EDOOFUS #{op.inspect}"
183       end
184     }.join('')
186     l = @logger
187     if l
188       l << str
189     else
190       env['rack.errors'].write(str)
191     end
192     nil
193   end