remove Clogger::ToPath proxy class
[clogger.git] / lib / clogger / pure.rb
blob661368662e0e62c5eb46861bdbe99e44a1b5c03a
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.new({})
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 private
94   def byte_xs(s)
95     s = s.dup
96     s.force_encoding(Encoding::BINARY) if defined?(Encoding::BINARY)
97     s.gsub!(/(['"\x00-\x1f])/) { |x| "\\x#{$1.unpack('H2').first.upcase}" }
98     s
99   end
101   SPECIAL_RMAP = SPECIAL_VARS.inject([]) { |ary, (k,v)| ary[v] = k; ary }
103   def request_uri(env)
104     ru = env['REQUEST_URI'] and return byte_xs(ru)
105     qs = env['QUERY_STRING']
106     qs.empty? or qs = "?#{byte_xs(qs)}"
107     "#{byte_xs(env['PATH_INFO'])}#{qs}"
108   end
110   def special_var(special_nr, env, status, headers)
111     case SPECIAL_RMAP[special_nr]
112     when :body_bytes_sent
113       @body_bytes_sent.to_s
114     when :status
115       status = status.to_i
116       status >= 100 && status <= 999 ? ('%03d' % status) : '-'
117     when :request
118       version = env['HTTP_VERSION'] and version = " #{byte_xs(version)}"
119       qs = env['QUERY_STRING']
120       qs.empty? or qs = "?#{byte_xs(qs)}"
121       "#{env['REQUEST_METHOD']} " \
122         "#{request_uri(env)}#{version}"
123     when :request_uri
124       request_uri(env)
125     when :request_length
126       env['rack.input'].size.to_s
127     when :response_length
128       @body_bytes_sent == 0 ? '-' : @body_bytes_sent.to_s
129     when :ip
130       xff = env['HTTP_X_FORWARDED_FOR'] and return byte_xs(xff)
131       env['REMOTE_ADDR'] || '-'
132     when :pid
133       $$.to_s
134     else
135       raise "EDOOFUS #{special_nr}"
136     end
137   end
139   def time_format(sec, usec, format, div)
140     format % [ sec, usec / div ]
141   end
143   def log(env, status, headers)
144     (@logger || env['rack.errors']) << @fmt_ops.map { |op|
145       case op[0]
146       when OP_LITERAL; op[1]
147       when OP_REQUEST; byte_xs(env[op[1]] || "-")
148       when OP_RESPONSE; byte_xs(headers[op[1]] || "-")
149       when OP_SPECIAL; special_var(op[1], env, status, headers)
150       when OP_EVAL; eval(op[1]).to_s rescue "-"
151       when OP_TIME_LOCAL; Time.now.strftime(op[1])
152       when OP_TIME_UTC; Time.now.utc.strftime(op[1])
153       when OP_REQUEST_TIME
154         t = Time.now - @start
155         time_format(t.to_i, (t - t.to_i) * 1000000, op[1], op[2])
156       when OP_TIME
157         t = Time.now
158         time_format(t.to_i, t.usec, op[1], op[2])
159       when OP_COOKIE
160         (env['rack.request.cookie_hash'][op[1]] rescue "-") || "-"
161       else
162         raise "EDOOFUS #{op.inspect}"
163       end
164     }.join('')
165   end