switch to TypedData macros for allocation
[clogger.git] / lib / clogger.rb
blob7ce2b2469898eb967c7f32c1e0fec25aaee630d9
1 # -*- encoding: binary -*-
2 require 'rack'
4 # See the README for usage instructions
5 class Clogger
7   # :stopdoc:
8   OP_LITERAL = 0
9   OP_REQUEST = 1
10   OP_RESPONSE = 2
11   OP_SPECIAL = 3
12   OP_EVAL = 4
13   OP_TIME_LOCAL = 5
14   OP_TIME_UTC = 6
15   OP_REQUEST_TIME = 7
16   OP_TIME = 8
17   OP_COOKIE = 9
19   # support nginx variables that are less customizable than our own
20   ALIASES = {
21     '$request_time' => '$request_time{3}',
22     '$msec' => '$time{3}',
23     '$usec' => '$time{6}',
24     '$http_content_length' => '$content_length',
25     '$http_content_type' => '$content_type',
26   }
28   SPECIAL_VARS = {
29     :body_bytes_sent => 0,
30     :status => 1,
31     :request => 2, # REQUEST_METHOD PATH_INFO?QUERY_STRING HTTP_VERSION
32     :request_length => 3, # env['rack.input'].size
33     :response_length => 4, # like body_bytes_sent, except "-" instead of "0"
34     :ip => 5, # HTTP_X_FORWARDED_FOR || REMOTE_ADDR || -
35     :pid => 6, # getpid()
36     :request_uri => 7,
37     :time_iso8601 => 8,
38     :time_local => 9,
39     :time_utc => 10,
40   }
42 private
44   CGI_ENV = Regexp.new('\A\$(' <<
45       %w(request_method content_length content_type
46          remote_addr remote_ident remote_user
47          path_info query_string script_name
48          server_name server_port
49          auth_type gateway_interface server_software path_translated
50          ).join('|') << ')\z')
52   SCAN = /([^$]*)(\$+(?:env\{\w+(?:\.[\w\.]+)?\}|
53                         e\{[^\}]+\}|
54                         (?:request_)?time\{\d+(?:,\d+)?\}|
55                         time_(?:utc|local)\{[^\}]+\}|
56                         \w*))?([^$]*)/x
58   def compile_format(str, opt = {})
59     str = Clogger::Format.const_get(str) if Symbol === str
60     longest_day = Time.at(26265600) # "Saturday, November 01, 1970 00:00:00"
61     rv = []
62     opt ||= {}
63     str.scan(SCAN).each do |pre,tok,post|
64       rv << [ OP_LITERAL, pre ] if pre && pre != ""
66       unless tok.nil?
67         if tok.sub!(/\A(\$+)\$/, '$')
68           rv << [ OP_LITERAL, $1 ]
69         end
71         compat = ALIASES[tok] and tok = compat
73         case tok
74         when /\A(\$*)\z/
75           rv << [ OP_LITERAL, $1 ]
76         when /\A\$env\{(\w+(?:\.[\w\.]+))\}\z/
77           rv << [ OP_REQUEST, $1 ]
78         when /\A\$e\{([^\}]+)\}\z/
79           rv << [ OP_EVAL, $1 ]
80         when /\A\$cookie_(\w+)\z/
81           rv << [ OP_COOKIE, $1 ]
82         when CGI_ENV, /\A\$(http_\w+)\z/
83           rv << [ OP_REQUEST, $1.upcase ]
84         when /\A\$sent_http_(\w+)\z/
85           rv << [ OP_RESPONSE, $1.downcase.tr('_','-') ]
86         when /\A\$time_local\{([^\}]+)\}\z/
87           fmt = $1
88           rv << [ OP_TIME_LOCAL, fmt, longest_day.strftime(fmt) ]
89         when /\A\$time_utc\{([^\}]+)\}\z/
90           fmt = $1
91           rv << [ OP_TIME_UTC, fmt, longest_day.strftime(fmt) ]
92         when /\A\$time\{(\d+)\}\z/
93           rv << [ OP_TIME, *usec_conv_pair(tok, $1.to_i) ]
94         when /\A\$request_time\{(\d+)\}\z/
95           rv << [ OP_REQUEST_TIME, *usec_conv_pair(tok, $1.to_i), 0 ]
96         when /\A\$request_time\{(\d+),(\d+)\}\z/
97           ipow = $1.to_i
98           prec = $2.to_i
99           if ipow > 9 # nanosecond precision is the highest POSIX goes
100             raise ArgumentError, "#{tok}: too big: #{ipow} (max=9)"
101           end
102           rv << [ OP_REQUEST_TIME, *usec_conv_pair(tok, prec), ipow ]
103         else
104           tok_sym = tok[1..-1].to_sym
105           if special_code = SPECIAL_VARS[tok_sym]
106             rv << [ OP_SPECIAL, special_code ]
107           else
108             raise ArgumentError, "unable to make sense of token: #{tok}"
109           end
110         end
111       end
113       rv << [ OP_LITERAL, post ] if post && post != ""
114     end
116     # auto-append a newline
117     last = rv.last or return rv
118     op = last.first
119     ors = opt[:ORS] || "\n"
120     if (op == OP_LITERAL && /#{ors}\z/ !~ last.last) || op != OP_LITERAL
121       rv << [ OP_LITERAL, ors ] if ors.size > 0
122     end
124     rv
125   end
127   def usec_conv_pair(tok, prec)
128     if prec == 0
129       [ "%d", 1 ] # stupid...
130     elsif prec > 6
131       raise ArgumentError, "#{tok}: too high precision: #{prec} (max=6)"
132     else
133       [ "%d.%0#{prec}d", 10 ** (6 - prec) ]
134     end
135   end
137   def need_response_headers?(fmt_ops)
138     fmt_ops.any? { |op| OP_RESPONSE == op[0] }
139   end
141   def need_wrap_body?(fmt_ops)
142     fmt_ops.any? do |op|
143       (OP_REQUEST_TIME == op[0]) || (OP_SPECIAL == op[0] &&
144         (SPECIAL_VARS[:body_bytes_sent] == op[1] ||
145          SPECIAL_VARS[:response_length] == op[1]))
146     end
147   end
149 private
150   def method_missing(*args, &block)
151     body.__send__(*args, &block)
152   end
153   # :startdoc:
156 require 'clogger/format'
158 begin
159   raise LoadError if ENV['CLOGGER_PURE'].to_i != 0
160   require 'clogger_ext'
161 rescue LoadError
162   require 'clogger/pure'