pure: fix reentrancy of request_time
[clogger.git] / lib / clogger / pure.rb
blob9efb00c4272a80d6db4f58756807c8cf19b31d0c
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, :start
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, {}, start)
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.start = start
43       wbody.env = env
44       wbody.status = status
45       wbody.headers = headers
46       wbody.body = body
47       return [ status, headers, wbody ]
48     end
49     log(env, status, headers, start)
50     [ status, headers, body ]
51   end
53   def each
54     @body_bytes_sent = 0
55     @body.each do |part|
56       @body_bytes_sent += Rack::Utils.bytesize(part)
57       yield part
58     end
59     self
60   end
62   def close
63     @body.close if @body.respond_to?(:close)
64   ensure
65     log(@env, @status, @headers)
66   end
68   def reentrant?
69     @reentrant
70   end
72   def wrap_body?
73     @wrap_body
74   end
76   def fileno
77     @logger.respond_to?(:fileno) ? @logger.fileno : nil
78   end
80   def respond_to?(m)
81     :close == m.to_sym || @body.respond_to?(m)
82   end
84   def to_path
85     rv = @body.to_path
86     @body_bytes_sent = File.size(rv)
87     rv
88   end
90 private
92   def byte_xs(s)
93     s = s.dup
94     s.force_encoding(Encoding::BINARY) if defined?(Encoding::BINARY)
95     s.gsub!(/(['"\x00-\x1f\x7f-\xff])/) do |x|
96       "\\x#{$1.unpack('H2').first.upcase}"
97     end
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     when :time_iso8601
135       Time.now.iso8601
136     when :time_local
137       t = Time.now
138       off = t.utc_offset
139       sign = off < 0 ? '-' : '+'
140       sprintf("%02d/%s/%d:%02d:%02d:%02d #{sign}%02d%02d",
141               t.mday, Time::RFC2822_MONTH_NAME[t.mon - 1],
142               t.year, t.hour, t.min, t.sec, *(off.abs / 60).divmod(60))
143     when :time_utc
144       t = Time.now.utc
145       sprintf("%02d/%s/%d:%02d:%02d:%02d +0000",
146               t.mday, Time::RFC2822_MONTH_NAME[t.mon - 1],
147               t.year, t.hour, t.min, t.sec)
148     else
149       raise "EDOOFUS #{special_nr}"
150     end
151   end
153   def time_format(sec, usec, format, div)
154     format % [ sec, usec / div ]
155   end
157   def log(env, status, headers, start = @start)
158     str = @fmt_ops.map { |op|
159       case op[0]
160       when OP_LITERAL; op[1]
161       when OP_REQUEST; byte_xs(env[op[1]] || "-")
162       when OP_RESPONSE; byte_xs(headers[op[1]] || "-")
163       when OP_SPECIAL; special_var(op[1], env, status, headers)
164       when OP_EVAL; eval(op[1]).to_s rescue "-"
165       when OP_TIME_LOCAL; Time.now.strftime(op[1])
166       when OP_TIME_UTC; Time.now.utc.strftime(op[1])
167       when OP_REQUEST_TIME
168         t = Time.now - start
169         time_format(t.to_i, (t - t.to_i) * 1000000, op[1], op[2])
170       when OP_TIME
171         t = Time.now
172         time_format(t.to_i, t.usec, op[1], op[2])
173       when OP_COOKIE
174         (byte_xs(env['rack.request.cookie_hash'][op[1]]) rescue "-") || "-"
175       else
176         raise "EDOOFUS #{op.inspect}"
177       end
178     }.join('')
180     l = @logger
181     if l
182       l << str
183     else
184       env['rack.errors'].write(str)
185     end
186     nil
187   end