add support for $time_iso8601 format
[clogger.git] / lib / clogger / pure.rb
bloba56b9824bf943c5aee8e440fd85d830e1b591160
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])/) { |x| "\\x#{$1.unpack('H2').first.upcase}" }
102     s
103   end
105   SPECIAL_RMAP = SPECIAL_VARS.inject([]) { |ary, (k,v)| ary[v] = k; ary }
107   def request_uri(env)
108     ru = env['REQUEST_URI'] and return byte_xs(ru)
109     qs = env['QUERY_STRING']
110     qs.empty? or qs = "?#{byte_xs(qs)}"
111     "#{byte_xs(env['PATH_INFO'])}#{qs}"
112   end
114   def special_var(special_nr, env, status, headers)
115     case SPECIAL_RMAP[special_nr]
116     when :body_bytes_sent
117       @body_bytes_sent.to_s
118     when :status
119       status = status.to_i
120       status >= 100 && status <= 999 ? ('%03d' % status) : '-'
121     when :request
122       version = env['HTTP_VERSION'] and version = " #{byte_xs(version)}"
123       qs = env['QUERY_STRING']
124       qs.empty? or qs = "?#{byte_xs(qs)}"
125       "#{env['REQUEST_METHOD']} " \
126         "#{request_uri(env)}#{version}"
127     when :request_uri
128       request_uri(env)
129     when :request_length
130       env['rack.input'].size.to_s
131     when :response_length
132       @body_bytes_sent == 0 ? '-' : @body_bytes_sent.to_s
133     when :ip
134       xff = env['HTTP_X_FORWARDED_FOR'] and return byte_xs(xff)
135       env['REMOTE_ADDR'] || '-'
136     when :pid
137       $$.to_s
138     when :time_iso8601
139       Time.now.iso8601
140     else
141       raise "EDOOFUS #{special_nr}"
142     end
143   end
145   def time_format(sec, usec, format, div)
146     format % [ sec, usec / div ]
147   end
149   def log(env, status, headers)
150     (@logger || env['rack.errors']) << @fmt_ops.map { |op|
151       case op[0]
152       when OP_LITERAL; op[1]
153       when OP_REQUEST; byte_xs(env[op[1]] || "-")
154       when OP_RESPONSE; byte_xs(headers[op[1]] || "-")
155       when OP_SPECIAL; special_var(op[1], env, status, headers)
156       when OP_EVAL; eval(op[1]).to_s rescue "-"
157       when OP_TIME_LOCAL; Time.now.strftime(op[1])
158       when OP_TIME_UTC; Time.now.utc.strftime(op[1])
159       when OP_REQUEST_TIME
160         t = Time.now - @start
161         time_format(t.to_i, (t - t.to_i) * 1000000, op[1], op[2])
162       when OP_TIME
163         t = Time.now
164         time_format(t.to_i, t.usec, op[1], op[2])
165       when OP_COOKIE
166         (env['rack.request.cookie_hash'][op[1]] rescue "-") || "-"
167       else
168         raise "EDOOFUS #{op.inspect}"
169       end
170     }.join('')
171   end