Rename parser -> Parser and formats -> FORMATS in apachelog.parser.
[apachelog.git] / apachelog / parser.py
1 import re
2
3
4 class ApacheLogParserError(Exception):
5     pass
6
7
8 class AttrDict(dict):
9     """
10     Allows dicts to be accessed via dot notation as well as subscripts
11     Makes using the friendly names nicer
12     """
13     def __getattr__(self, name):
14         return self[name]
15
16 """
17 Frequenty used log formats stored here
18 """
19 FORMATS = {
20     # Common Log Format (CLF)
21     'common':r'%h %l %u %t \"%r\" %>s %b',
22
23     # Common Log Format with Virtual Host
24     'vhcommon':r'%v %h %l %u %t \"%r\" %>s %b',
25
26     # NCSA extended/combined log format
27     'extended':r'%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"',
28     }
29
30
31 class Parser (object):
32     format_to_name = {
33         # Explanatory comments copied from
34         # http://httpd.apache.org/docs/2.2/mod/mod_log_config.html
35         # Remote IP-address
36         '%a':'remote_ip',
37         # Local IP-address
38         '%A':'local_ip',
39         # Size of response in bytes, excluding HTTP headers.
40         '%B':'response_bytes',
41         # Size of response in bytes, excluding HTTP headers. In CLF
42         # format, i.e. a "-" rather than a 0 when no bytes are sent.
43         '%b':'response_bytes_clf',
44         # The contents of cookie Foobar in the request sent to the server.
45         # Only version 0 cookies are fully supported.
46         #'%{Foobar}C':'',
47         '%{}C':'cookie',
48         # The time taken to serve the request, in microseconds.
49         '%D':'response_time_us',
50         # The contents of the environment variable FOOBAR
51         #'%{FOOBAR}e':'',
52         '%{}e':'env',
53         # Filename
54         '%f':'filename',
55         # Remote host
56         '%h':'remote_host',
57         # The request protocol
58         '%H':'request_protocol',
59         # The contents of Foobar: header line(s) in the request sent to
60         # the server. Changes made by other modules (e.g. mod_headers)
61         # affect this.
62         #'%{Foobar}i':'',
63         '%{}i':'header',
64         # Number of keepalive requests handled on this connection.
65         # Interesting if KeepAlive is being used, so that, for example,
66         # a "1" means the first keepalive request after the initial one,
67         # "2" the second, etc...; otherwise this is always 0 (indicating
68         # the initial request). Available in versions 2.2.11 and later.
69         '%k':'keepalive_num',
70         # Remote logname (from identd, if supplied). This will return a
71         # dash unless mod_ident is present and IdentityCheck is set On.
72         '%l':'remote_logname',
73         # The request method
74         '%m':'request_method',
75         # The contents of note Foobar from another module.
76         #'%{Foobar}n':'',
77         '%{}n':'note',
78         # The contents of Foobar: header line(s) in the reply.
79         #'%{Foobar}o':'',
80         '%{}o':'reply_header',
81         # The canonical port of the server serving the request
82         '%p':'server_port',
83         # The canonical port of the server serving the request or the
84         # server's actual port or the client's actual port. Valid
85         # formats are canonical, local, or remote.
86         #'%{format}p':"",
87         '%{}p':'port',
88         # The process ID of the child that serviced the request.
89         '%P':'process_id',
90         # The process ID or thread id of the child that serviced the
91         # request. Valid formats are pid, tid, and hextid. hextid requires
92         # APR 1.2.0 or higher.
93         #'%{format}P':'',
94         '%{}P':'pid',
95         # The query string (prepended with a ? if a query string exists,
96         # otherwise an empty string)
97         '%q':'query_string',
98         # First line of request
99         # e.g., what you'd see in the logs as 'GET / HTTP/1.1'
100         '%r':'first_line',
101         # The handler generating the response (if any).
102         '%R':'response_handler',
103         # Status. For requests that got internally redirected, this is
104         # the status of the *original* request --- %>s for the last.
105         '%s':'status',
106         '%>s':'last_status',
107         # Time the request was received (standard english format)
108         '%t':'time',
109         # The time, in the form given by format, which should be in
110         # strftime(3) format. (potentially localized)
111         #'%{format}t':'TODO',
112         # The time taken to serve the request, in seconds.
113         '%T':'response_time_sec',
114         # Remote user (from auth; may be bogus if return status (%s) is 401)
115         '%u':'remote_user',
116         # The URL path requested, not including any query string.
117         '%U':'url_path',
118         # The canonical ServerName of the server serving the request.
119         '%v':'canonical_server_name',
120         # The server name according to the UseCanonicalName setting.
121         '%V':'server_name_config', #TODO: Needs better name
122         # Connection status when response is completed:
123         # X = connection aborted before the response completed.
124         # + = connection may be kept alive after the response is sent.
125         # - = connection will be closed after the response is sent.
126         '%X':'completed_connection_status',
127         # Bytes received, including request and headers, cannot be zero.
128         # You need to enable mod_logio to use this.
129         '%I':'bytes_received',
130         # Bytes sent, including headers, cannot be zero. You need to
131         # enable mod_logio to use this
132         '%O':'bytes_sent',
133     }
134
135     def __init__(self, format, use_friendly_names=False):
136         """
137         Takes the log format from an Apache configuration file.
138
139         Best just copy and paste directly from the .conf file
140         and pass using a Python raw string e.g.
141
142         format = r'%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"'
143         p = apachelog.parser(format)
144         """
145         self._names = []
146         self._regex = None
147         self._pattern = ''
148         self._use_friendly_names = use_friendly_names
149         self._parse_format(format)
150
151     def _parse_format(self, format):
152         """
153         Converts the input format to a regular
154         expression, as well as extracting fields
155
156         Raises an exception if it couldn't compile
157         the generated regex.
158         """
159         format = format.strip()
160         format = re.sub('[ \t]+',' ',format)
161
162         subpatterns = []
163
164         findquotes = re.compile(r'^\\"')
165         findreferreragent = re.compile('Referer|User-Agent', re.I)
166         findpercent = re.compile('^%.*t$')
167         lstripquotes = re.compile(r'^\\"')
168         rstripquotes = re.compile(r'\\"$')
169         self._names = []
170
171         for element in format.split(' '):
172
173             hasquotes = 0
174             if findquotes.search(element): hasquotes = 1
175
176             if hasquotes:
177                 element = lstripquotes.sub('', element)
178                 element = rstripquotes.sub('', element)
179
180             if self._use_friendly_names:
181                 self._names.append(self.alias(element))
182             else:
183                 self._names.append(element)
184
185             subpattern = '(\S*)'
186
187             if hasquotes:
188                 if element == '%r' or findreferreragent.search(element):
189                     subpattern = r'\"([^"\\]*(?:\\.[^"\\]*)*)\"'
190                 else:
191                     subpattern = r'\"([^\"]*)\"'
192
193             elif findpercent.search(element):
194                 subpattern = r'(\[[^\]]+\])'
195
196             elif element == '%U':
197                 subpattern = '(.+?)'
198
199             subpatterns.append(subpattern)
200
201         self._pattern = '^' + ' '.join(subpatterns) + '$'
202         try:
203             self._regex = re.compile(self._pattern)
204         except Exception, e:
205             raise ApacheLogParserError(e)
206
207     def parse(self, line):
208         """
209         Parses a single line from the log file and returns
210         a dictionary of it's contents.
211
212         Raises and exception if it couldn't parse the line
213         """
214         line = line.strip()
215         match = self._regex.match(line)
216
217         if match:
218             data = AttrDict()
219             for k, v in zip(self._names, match.groups()):
220                 data[k] = v
221             return data
222
223         raise ApacheLogParserError("Unable to parse: %s with the %s regular expression" % ( line, self._pattern ) )
224
225     def alias(self, name):
226         """
227         Override / replace this method if you want to map format
228         field names to something else. This method is called
229         when the parser is constructed, not when actually parsing
230         a log file
231
232         For custom format names, such as %{Foobar}C, 'Foobar' is referred to
233         (in this function) as the custom_format and '%{}C' as the name
234
235         If the custom_format has a '-' in it (and is not a time format), then the
236         '-' is replaced with a '_' so the name remains a valid identifier.
237
238         Takes and returns a string fieldname
239         """
240
241         custom_format = ''
242
243         if name.startswith('%{'):
244             custom_format = '_' + name[2:-2]
245             name = '%{}' + name[-1]
246
247             if name != '%{}t':
248                 custom_format = custom_format.replace('-', '_')
249
250         try:
251             return self.format_to_name[name] + custom_format
252         except KeyError:
253             return name
254
255     def pattern(self):
256         """
257         Returns the compound regular expression the parser extracted
258         from the input format (a string)
259         """
260         return self._pattern
261
262     def names(self):
263         """
264         Returns the field names the parser extracted from the
265         input format (a list)
266         """
267         return self._names