remove :to_io support
[clogger.git] / lib / clogger / pure.rb
blobbb3fc16eb63e132ed3b5692bf52bb8ace7233e28
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     @body_bytes_sent = File.size(rv)
86     rv
87   end
89 private
91   def byte_xs(s)
92     s = s.dup
93     s.force_encoding(Encoding::BINARY) if defined?(Encoding::BINARY)
94     s.gsub!(/(['"\x00-\x1f\x7f-\xff])/) do |x|
95       "\\x#{$1.unpack('H2').first.upcase}"
96     end
97     s
98   end
100   SPECIAL_RMAP = SPECIAL_VARS.inject([]) { |ary, (k,v)| ary[v] = k; ary }
102   def request_uri(env)
103     ru = env['REQUEST_URI'] and return byte_xs(ru)
104     qs = env['QUERY_STRING']
105     qs.empty? or qs = "?#{byte_xs(qs)}"
106     "#{byte_xs(env['PATH_INFO'])}#{qs}"
107   end
109   def special_var(special_nr, env, status, headers)
110     case SPECIAL_RMAP[special_nr]
111     when :body_bytes_sent
112       @body_bytes_sent.to_s
113     when :status
114       status = status.to_i
115       status >= 100 && status <= 999 ? ('%03d' % status) : '-'
116     when :request
117       version = env['HTTP_VERSION'] and version = " #{byte_xs(version)}"
118       qs = env['QUERY_STRING']
119       qs.empty? or qs = "?#{byte_xs(qs)}"
120       "#{env['REQUEST_METHOD']} " \
121         "#{request_uri(env)}#{version}"
122     when :request_uri
123       request_uri(env)
124     when :request_length
125       env['rack.input'].size.to_s
126     when :response_length
127       @body_bytes_sent == 0 ? '-' : @body_bytes_sent.to_s
128     when :ip
129       xff = env['HTTP_X_FORWARDED_FOR'] and return byte_xs(xff)
130       env['REMOTE_ADDR'] || '-'
131     when :pid
132       $$.to_s
133     when :time_iso8601
134       Time.now.iso8601
135     when :time_local
136       t = Time.now
137       off = t.utc_offset
138       sign = off < 0 ? '-' : '+'
139       sprintf("%02d/%s/%d:%02d:%02d:%02d #{sign}%02d%02d",
140               t.mday, Time::RFC2822_MONTH_NAME[t.mon - 1],
141               t.year, t.hour, t.min, t.sec, *(off.abs / 60).divmod(60))
142     when :time_utc
143       t = Time.now.utc
144       sprintf("%02d/%s/%d:%02d:%02d:%02d +0000",
145               t.mday, Time::RFC2822_MONTH_NAME[t.mon - 1],
146               t.year, t.hour, t.min, t.sec)
147     else
148       raise "EDOOFUS #{special_nr}"
149     end
150   end
152   def time_format(sec, usec, format, div)
153     format % [ sec, usec / div ]
154   end
156   def log(env, status, headers)
157     str = @fmt_ops.map { |op|
158       case op[0]
159       when OP_LITERAL; op[1]
160       when OP_REQUEST; byte_xs(env[op[1]] || "-")
161       when OP_RESPONSE; byte_xs(headers[op[1]] || "-")
162       when OP_SPECIAL; special_var(op[1], env, status, headers)
163       when OP_EVAL; eval(op[1]).to_s rescue "-"
164       when OP_TIME_LOCAL; Time.now.strftime(op[1])
165       when OP_TIME_UTC; Time.now.utc.strftime(op[1])
166       when OP_REQUEST_TIME
167         t = Time.now - @start
168         time_format(t.to_i, (t - t.to_i) * 1000000, op[1], op[2])
169       when OP_TIME
170         t = Time.now
171         time_format(t.to_i, t.usec, op[1], op[2])
172       when OP_COOKIE
173         (byte_xs(env['rack.request.cookie_hash'][op[1]]) rescue "-") || "-"
174       else
175         raise "EDOOFUS #{op.inspect}"
176       end
177     }.join('')
179     l = @logger
180     if l
181       l << str
182     else
183       env['rack.errors'].write(str)
184     end
185     nil
186   end