11# coding: utf-8
22
3- from __future__ import unicode_literals
3+ from __future__ import absolute_import , unicode_literals
4+
5+ from functools import partial
46from pprint import pformat
5- from boxsdk .network .default_network import DefaultNetwork
7+ import sys
8+
9+ from six import text_type
10+
11+ from boxsdk .network .default_network import DefaultNetwork , DefaultNetworkResponse
612from boxsdk .util .log import setup_logging
713
814
@@ -11,10 +17,11 @@ class LoggingNetwork(DefaultNetwork):
1117 SDK Network subclass that logs requests and responses.
1218 """
1319 LOGGER_NAME = 'boxsdk.network'
14- REQUEST_FORMAT = '\x1b [36m%s %s %s\x1b [0m'
15- SUCCESSFUL_RESPONSE_FORMAT = '\x1b [32m%s\x1b [0m'
16- ERROR_RESPONSE_FORMAT = '\x1b [31m%s\n %s\n %s\n \x1b [0m'
17- STREAM_CONTENT_NOT_LOGGED = '<File download contents unavailable for logging>'
20+ REQUEST_FORMAT = '\x1b [36m%(method)s %(url)s %(request_kwargs)s\x1b [0m'
21+ EXCEPTION_FORMAT = '\x1b [31mRequest "%(method)s %(url)s" failed with %(exc_type_name)s exception: %(exc_value)r\x1b [0m'
22+ _COMMON_RESPONSE_FORMAT = '"%(method)s %(url)s" %(status_code)s %(content_length)s\n %(headers)s\n %(content)s\n '
23+ SUCCESSFUL_RESPONSE_FORMAT = '\x1b [32m{0}\x1b [0m' .format (_COMMON_RESPONSE_FORMAT )
24+ ERROR_RESPONSE_FORMAT = '\x1b [31m{0}\x1b [0m' .format (_COMMON_RESPONSE_FORMAT )
1825
1926 def __init__ (self , logger = None ):
2027 """
@@ -48,32 +55,216 @@ def _log_request(self, method, url, **kwargs):
4855 :type access_token:
4956 `unicode`
5057 """
51- self ._logger .info (self .REQUEST_FORMAT , method , url , pformat (kwargs ))
58+ self ._logger .info (self .REQUEST_FORMAT , { ' method' : method , ' url' : url , 'request_kwargs' : pformat (kwargs )} )
5259
53- def _log_response (self , response ):
54- """
55- Logs information about the Box API response.
60+ def _log_exception (self , method , url , exc_info ):
61+ """Log information at WARNING level about the exception that was raised when trying to make the request.
5662
57- :param response: The Box API response.
63+ :param method: The HTTP verb that was used to make the request.
64+ :type method: `unicode`
65+ :param url: The URL for the request.
66+ :type url: `unicode`
67+ :param exc_info: The exception info returned from `sys.exc_info()`.
5868 """
59- if response .ok :
60- if response .request_response .raw is not None :
61- self ._logger .info (self .SUCCESSFUL_RESPONSE_FORMAT , self .STREAM_CONTENT_NOT_LOGGED )
62- else :
63- self ._logger .info (self .SUCCESSFUL_RESPONSE_FORMAT , response .content )
64- else :
65- self ._logger .warning (
66- self .ERROR_RESPONSE_FORMAT ,
67- response .status_code ,
68- response .headers ,
69- pformat (response .content ),
70- )
69+ exc_type , exc_value , _ = exc_info
70+ self ._logger .warning (
71+ self .EXCEPTION_FORMAT ,
72+ {'method' : method , 'url' : url , 'exc_type_name' : exc_type .__name__ , 'exc_value' : exc_value },
73+ )
7174
7275 def request (self , method , url , access_token , ** kwargs ):
7376 """
7477 Base class override. Logs information about an API request and response in addition to making the request.
78+
79+ Also logs exceptions before re-raising them.
80+
81+ The logging of the response is deferred to
82+ :class:`LoggingNetworkResponse`. See that class's docstring for more
83+ info.
7584 """
7685 self ._log_request (method , url , ** kwargs )
77- response = super (LoggingNetwork , self ).request (method , url , access_token , ** kwargs )
78- self ._log_response (response )
86+ try :
87+ return super (LoggingNetwork , self ).request (method , url , access_token , ** kwargs )
88+ except Exception :
89+ self ._log_exception (method , url , sys .exc_info ())
90+ raise
91+
92+ @property
93+ def network_response_constructor (self ):
94+ """Baseclass override.
95+
96+ A callable that passes additional required keyword arguments to the
97+ :class:`LoggingNetworkResponse` constructor, and returns an instance of
98+ :class:`LoggingNetworkResponse`.
99+ """
100+ return partial (
101+ LoggingNetworkResponse ,
102+ logger = self ._logger ,
103+ successful_response_format = self .SUCCESSFUL_RESPONSE_FORMAT ,
104+ error_response_format = self .ERROR_RESPONSE_FORMAT ,
105+ )
106+
107+
108+ class LoggingNetworkResponse (DefaultNetworkResponse ):
109+ """Response subclass that defers LoggingNetwork response logging until it is safe to do so.
110+
111+ :class:`DefaultNetwork` is based off the `requests` library.
112+ :class:`requests.Response` has a few mutually-exclusive ways to read the
113+ content of the response:
114+
115+ - With the `Response.raw` attribute, an `io.IOBase` instance returned
116+ from the `urllib3` library, that can be read once in chunks from
117+ beginning to end.
118+ - With `Response.iter_content()` and other iter_* generators, which
119+ also can only be read once and advance the `Response.raw` IO stream.
120+ - With the `Response.content` property (and other attributes such as
121+ `Response.text` and `Response.json()`), which reads and caches the
122+ remaining response content in memory. Can be accessed multiple times,
123+ but cannot be safely accessed if any of the previous mechanisms have
124+ been used at all. And if this property has already been accessed,
125+ then the other mechanisms will have been exhausted, and attempting to
126+ read from them will make it appear like the response content is
127+ empty.
128+
129+ Any of these mechanisms may be used to read any response, regardless of
130+ whether `stream=True` or `stream=False` on the request.
131+
132+ If the caller uses `Response.content`, then it is safe for
133+ :class:`LoggingNetwork` to also access it. But if the caller uses any of
134+ the streaming mechanisms, then it is not safe for :class:`LoggingNetwork`
135+ to ever read any of the content. Thus, the options available are:
136+
137+ - Never log the content of a response.
138+ - Make logging part of the :class:`Network` interface, and add an
139+ optional keyword argument that callers can use to specify when it is
140+ unsafe to log the content of a response.
141+ - Defer logging until it is possible to auto-detect which mechanism is
142+ being used.
143+
144+ This class is an implementation of the latter option. Instead of response
145+ logging taking place in `LoggingNetwork.request()`, it takes place in this
146+ `DefaultNetworkResponse` subclass, as soon as the caller starts reading the
147+ content. If `content` or `json()` are accessed, then the response will be
148+ logged with its content. Whereas if `response_as_stream` or
149+ `request_response` are accessed, then the response will be logged with a
150+ placeholder for the actual content.
151+
152+ In theory, this could make the logs less useful, by adding a delay between
153+ when the network response was actually received, and when it is logged. Or
154+ the response may never be logged, if the content is never accessed. In
155+ practice, this is unlikely to happen, because nearly all SDK methods
156+ immediately read the content.
157+ """
158+
159+ STREAM_CONTENT_NOT_LOGGED = '<File download contents unavailable for logging>'
160+
161+ def __init__ (self , logger , successful_response_format , error_response_format , ** kwargs ):
162+ """Extends baseclass method.
163+
164+ :param logger: The logger to use.
165+ :type logger: :class:`Logger`
166+ :param successful_response_format:
167+ The logger %-style format string to use for logging ok responses.
168+
169+ May use the following format placeholders:
170+
171+ - %(method)s : The HTTP request method ('GET', 'POST', etc.).
172+ - %(url)s : The url of the request.
173+ - %(status_code)s : The HTTP status code of the response.
174+ - %(content_length)s : The Content-Length of the response body.
175+ - %(headers)s : The HTTP headers (as a pretty-printed dict).
176+ - %(content)s : The response body.
177+
178+ :type successful_response_format: `unicode`
179+ :param error_response_format:
180+ The logger %-style format string to use for logging ok responses.
181+
182+ May use the same format placeholders as
183+ `successful_response_format`.
184+ :type error_response_format: `unicode`
185+ """
186+ super (LoggingNetworkResponse , self ).__init__ (** kwargs )
187+ self ._logger = logger
188+ self ._successful_response_format = successful_response_format
189+ self ._error_response_format = error_response_format
190+ self ._did_log = False
191+
192+ def log (self , can_safely_log_content = False ):
193+ """Logs information about the Box API response.
194+
195+ Will only execute once. Subsequent calls will be no-ops. This is
196+ partially because we only want to log responses once, and partially
197+ because this is necessary to prevent this method from infinite
198+ recursing with its use of the `content` property.
199+
200+ :param can_safely_log_content:
201+ (optional) `True` if the caller is accessing the `content`
202+ property, `False` otherwise.
203+
204+ As stated in the class docstring, it is unsafe for this logging
205+ method to access `content` unless the caller is also accessing it.
206+
207+ Defaults to `False`.
208+ :type can_safely_log_content: `bool`
209+ """
210+ if self ._did_log :
211+ return
212+ self ._did_log = True
213+ content_length = self .headers .get ('Content-Length' , None )
214+ content = self .STREAM_CONTENT_NOT_LOGGED
215+ if can_safely_log_content :
216+ if content_length is None :
217+ content_length = text_type (len (self .content ))
218+
219+ # If possible, get the content as a JSON `dict`, that way
220+ # `pformat(content)` will return pretty-printed JSON.
221+ try :
222+ content = self .json ()
223+ except ValueError :
224+ content = self .content
225+ content = pformat (content )
226+ if content_length is None :
227+ content_length = '?'
228+ if self .ok :
229+ logger_method , response_format = self ._logger .info , self ._successful_response_format
230+ else :
231+ logger_method , response_format = self ._logger .warning , self ._error_response_format
232+ logger_method (
233+ response_format ,
234+ {
235+ 'method' : self .request_response .request .method ,
236+ 'url' : self .request_response .request .url ,
237+ 'status_code' : self .status_code ,
238+ 'content_length' : content_length ,
239+ 'headers' : pformat (self .headers ),
240+ 'content' : content ,
241+ },
242+ )
243+
244+ def json (self ):
245+ """Extends baseclass method."""
246+ try :
247+ return super (LoggingNetworkResponse , self ).json ()
248+ finally :
249+ self .log (can_safely_log_content = True )
250+
251+ @property
252+ def content (self ):
253+ """Extends baseclass method."""
254+ content = super (LoggingNetworkResponse , self ).content
255+ self .log (can_safely_log_content = True )
256+ return content
257+
258+ @property
259+ def response_as_stream (self ):
260+ """Extends baseclass method."""
261+ stream = super (LoggingNetworkResponse , self ).response_as_stream
262+ self .log (can_safely_log_content = False )
263+ return stream
264+
265+ @property
266+ def request_response (self ):
267+ """Extends baseclass method."""
268+ response = super (LoggingNetworkResponse , self ).request_response
269+ self .log (can_safely_log_content = False )
79270 return response
0 commit comments