1 # Copyright (C) 2014 Ipsilon Project Contributors
3 # See the file named COPYING for the project license
13 def log_request_response():
14 '''Log the contents of the request and subsequent response.
16 This is run as a tool hook and should be run as the last hook
17 (on_end_resource) just before the server writes the response to
18 the client. The tool is registered like this:
20 cherrypy.tools.log_request_response = \
21 cherrypy.Tool('on_end_resource', log_request_response)
23 Then the logging can be enabled or disabled via the config option,
24 for example to turn the logging on via a config dict add this
27 'tools.log_request_response.on': True,
29 or for a config file add this line:
31 tools.log_request_response.on = True
33 At first blush it would seem easy to log the request received and
34 the response emitted, just hook those locations and output the raw
35 data. Unfortunately there are no such locations in cherrypy where
36 you have access to the raw input and output data. The complicating
39 * Input data is consumed by the Request object off a file object
40 which cannot seek back (i.e. rewind), once the data is read it
41 cannot be read again. Therefore any attempt to log the raw input
42 will starve read operations done by the Request object to read
45 * Automatic consumption and processing of the body contents is
46 enabled by default (controlled by the
47 request.process_request_body config option). You generally do
48 not want to turn these automatic request processors off because
49 they provide valuable input processing useful to the request
50 processing pipeline (e.g. when Content-Type is
51 application/x-www-form-urlencoded, or multipart/form-data or
52 multipart). Most cherrypy page handlers expect this
53 pre-processing to have been performed and the 'cooked' data to
54 be availabe on the Request object. Thus rather than logging the
55 raw input HTTP which will have been consumed by the request
56 processing logic we are forced into logging only the 'cooked'
57 values available to us after the request has been read and
60 * The response body may not be available if response streaming is
61 enabled. This is because control is passed directly to the
62 object writing the data to the client bypassing the normal
63 cherrypy hooks. Fortunately streaming is not recommended and we
64 can expect it will be disabled. When streaming is disabled the
65 response body can be composed as:
69 - a generator yielding chunks of strings
71 When the response body is a string or list of strings logging
72 the body and then passing down the pipeline to be written to the
73 client is trivial. However when part of the body is produced by
74 a generator we must run the generator to produce that part of
75 the body and store it as a string. This is an issue equivalent
76 to not being able to re-read a file object as seen in the input
77 situation. Once the generator has run it cannot be run
78 again. Therefore we consume all the body output, store it in a
79 string, log it and then replace the request body contents with
80 the body string we formed. It's this body string which is
81 subsequently sent down the processing pipeline to be written to
86 # --- Begin local functions ---
88 def indent_text(text, level=0, indent=' '):
90 Input is a block of text potentially containing newlines which
91 seperate the text into a sequence of lines. The text block is
92 split into individual lines and indented according to the
93 indentation level. The width of the indent is controlled by
94 the optional indent parameter.
96 The result is a single block of text where each of the
97 original lines of text are indented.
100 f = cStringIO.StringIO()
102 lines = text.split('\n')
104 # Do not output trailing newline
105 if lines and lines[-1] == '':
109 f.write(indent*level)
113 string = f.getvalue()
117 def print_part(part):
119 Format a cherrypy._cpreqbody.Part object into a string.
121 When the request Content-Type is a multipart cherrypy splits
122 each part of the multipart into a Part object containing
123 information about the part and it's content.
125 f = cStringIO.StringIO()
127 f.write(indent_text('Name = %s\n' % part.name))
129 f.write(indent_text('Headers:\n'))
130 for name, value in part.headers.items():
131 f.write(indent_text('%s: %s\n' % (name, value), 1))
133 f.write(indent_text("Body:\n"))
134 f.write(indent_text(part.fullvalue(), 1))
136 string = f.getvalue()
140 def print_param(name, value):
141 f = cStringIO.StringIO()
143 # Might be a multipart Part object, if so format it
144 if isinstance(value, cherrypy._cpreqbody.Part): # pylint:disable=W0212
145 f.write(indent_text("%s:\n" % (name)))
146 f.write(indent_text(print_part(value), 1))
148 # Not a mulitpart, just write it as a string
149 f.write(indent_text("%s: %s\n" % (name, value)))
151 string = f.getvalue()
155 def collapse_body(body):
156 '''The cherrypy response body can be:
160 * generator yielding a string
162 Generators are typically used for file contents but any
163 cherrypy response is permitted to use a generator to provide
164 the body of the response.
166 Strings and lists of strings are immediately available and
167 stored in the request object. During normal cherrypy
168 processing when writing the response to the client response
169 data which is provided by a generator will be iterated over
170 and written to the client. In order for us to be able to log
171 all the response data prior to it being sent to the client we
172 must also iterate over the generator provided content, however
173 this exhausts the generator making it unavailable to be
174 written to the client.
176 To solve this problem we collect all the response data. Now we
177 have the full body contents, we can log it and then set this
178 as the new body contents for remainder of the processing
179 pipeline to act upon (i.e. sent to the client)
181 f = cStringIO.StringIO()
186 string = f.getvalue()
190 # --- End local functions ---
192 f = cStringIO.StringIO()
193 request = cherrypy.serving.request
194 remote = request.remote
199 f.write(indent_text("<Request> [%s] %s\n" %
200 (remote.name or remote.ip, request.request_line), 0))
204 f.write(indent_text("Headers:\n", 1))
205 for name, value in request.headers.items():
206 f.write(indent_text("%s: %s\n" % (name, value), 2))
208 # Request parameters from URL query string and
209 # x-www-form-urlencoded POST data
210 if request.body.params:
211 f.write(indent_text("Params:\n", 1))
212 for name, value in request.body.params.items():
213 # Multi-valued paramater is in a list
214 if isinstance(value, list):
215 for i, item in enumerate(value):
216 f.write(indent_text(print_param("%s[%d]" % (name, i),
219 f.write(indent_text(print_param(name, value), 2))
221 # If the body is multipart format each of the parts
222 if request.body.parts:
223 f.write(indent_text("Body Parts:\n"))
224 for i, part in enumerate(request.body.parts):
225 f.write(indent_text("Part %s name=%s:\n" % (i, part.name), 3))
226 f.write(indent_text(print_part(part), 4))
231 response = cherrypy.response
232 f.write(indent_text("<Response> %s\n" % response.status, 0))
234 # Log the response headers
235 if response.header_list:
236 f.write(indent_text("Headers:\n", 1))
237 for name, value in response.header_list:
238 f.write(indent_text("%s: %s\n" % (name, value), 2))
240 # Log the response body
242 # We can only do this if the response is not streaming because we have
243 # no way to hook the streaming content.
244 f.write(indent_text("Body:\n", 1))
247 f.write(indent_text("body omitted because response is streaming\n", 2))
249 response.body = collapse_body(response.body)
250 for chunk in response.body:
251 f.write(indent_text(chunk, 2))
253 string = f.getvalue()
257 cherrypy.tools.log_request_response = cherrypy.Tool('on_end_resource',
258 log_request_response)
265 buf = cStringIO.StringIO()
267 stack = traceback.extract_stack()
268 traceback.print_list(stack[:-2], file=buf)
270 stacktrace_string = buf.getvalue()
272 return stacktrace_string
275 def get_class_from_frame(frame_obj):
278 http://stackoverflow.com/questions/2203424/
279 python-how-to-retrieve-class-information-from-a-frame-object
281 At the frame object level, there does not seem to be any way
282 to find the actual python function object that has been
285 However, if your code relies on the common convention of naming
286 the instance parameter of a method self, then you could do this.
289 args, _, _, value_dict = inspect.getargvalues(frame_obj)
290 # Is the functions first parameter named 'self'?
291 if len(args) and args[0] == 'self':
292 # in that case, 'self' will be referenced in value_dict
293 instance = value_dict.get('self', None)
296 return getattr(instance, '__class__', None)
297 # return None otherwise
302 frame = inspect.stack()[2]
305 line_number = frame[2]
308 # Only report the last 3 components of the path
309 filename = os.sep.join(filename.split(os.sep)[-3:])
311 cls = Log.get_class_from_frame(frame_obj)
313 location = '%s:%s %s.%s()' % \
314 (filename, line_number, cls.__name__, func)
316 location = '%s:%s %s()' % (filename, line_number, func)
319 def debug(self, fact):
320 if cherrypy.config.get('debug', False):
321 location = Log.call_location()
322 cherrypy.log('DEBUG(%s): %s' % (location, fact))
324 # for compatibility with existing code
330 def error(self, fact):
331 cherrypy.log.error('ERROR: %s' % fact, severity=logging.ERROR)
332 if cherrypy.config.get('stacktrace_on_error', False):
333 cherrypy.log.error(Log.stacktrace())