1"""
2Additional handlers that are used as the base classes of the buildlogger
3handler.
4"""
5
6from __future__ import absolute_import
7
8import json
9import logging
10import threading
11import urllib2
12
13from .. import utils
14from ..utils import timer
15
16_TIMEOUT_SECS = 10
17
18class BufferedHandler(logging.Handler):
19    """
20    A handler class that buffers logging records in memory. Whenever
21    each record is added to the buffer, a check is made to see if the
22    buffer should be flushed. If it should, then flush() is expected to
23    do what's needed.
24    """
25
26    def __init__(self, capacity, interval_secs):
27        """
28        Initializes the handler with the buffer size and timeout after
29        which the buffer is flushed regardless.
30        """
31
32        logging.Handler.__init__(self)
33
34        if not isinstance(capacity, int):
35            raise TypeError("capacity must be an integer")
36        elif capacity <= 0:
37            raise ValueError("capacity must be a positive integer")
38
39        if not isinstance(interval_secs, (int, float)):
40            raise TypeError("interval_secs must be a number")
41        elif interval_secs <= 0.0:
42            raise ValueError("interval_secs must be a positive number")
43
44        self.capacity = capacity
45        self.interval_secs = interval_secs
46        self.buffer = []
47
48        self._lock = threading.Lock()
49        self._timer = None  # Defer creation until actually begin to log messages.
50
51    def _new_timer(self):
52        """
53        Returns a new timer.AlarmClock instance that will call the
54        flush() method after 'interval_secs' seconds.
55        """
56
57        return timer.AlarmClock(self.interval_secs, self.flush, args=[self])
58
59    def process_record(self, record):
60        """
61        Applies a transformation to the record before it gets added to
62        the buffer.
63
64        The default implementation returns 'record' unmodified.
65        """
66
67        return record
68
69    def emit(self, record):
70        """
71        Emits a record.
72
73        Append the record to the buffer after it has been transformed by
74        process_record(). If the length of the buffer is greater than or
75        equal to its capacity, then flush() is called to process the
76        buffer.
77
78        After flushing the buffer, the timer is restarted so that it
79        will expire after another 'interval_secs' seconds.
80        """
81
82        with self._lock:
83            self.buffer.append(self.process_record(record))
84            if len(self.buffer) >= self.capacity:
85                if self._timer is not None:
86                    self._timer.snooze()
87                self.flush_with_lock(False)
88                if self._timer is not None:
89                    self._timer.reset()
90
91        if self._timer is None:
92            self._timer = self._new_timer()
93            self._timer.start()
94
95    def flush(self, close_called=False):
96        """
97        Ensures all logging output has been flushed.
98        """
99
100        with self._lock:
101            if self.buffer:
102                self.flush_with_lock(close_called)
103
104    def flush_with_lock(self, close_called):
105        """
106        Ensures all logging output has been flushed.
107
108        This version resets the buffers back to an empty list and is
109        intended to be overridden by subclasses.
110        """
111
112        self.buffer = []
113
114    def close(self):
115        """
116        Tidies up any resources used by the handler.
117
118        Stops the timer and flushes the buffer.
119        """
120
121        if self._timer is not None:
122            self._timer.dismiss()
123        self.flush(close_called=True)
124
125        logging.Handler.close(self)
126
127
128class HTTPHandler(object):
129    """
130    A class which sends data to a web server using POST requests.
131    """
132
133    def __init__(self, realm, url_root, username, password):
134        """
135        Initializes the handler with the necessary authenticaton
136        credentials.
137        """
138
139        digest_handler = urllib2.HTTPDigestAuthHandler()
140        digest_handler.add_password(
141            realm=realm,
142            uri=url_root,
143            user=username,
144            passwd=password)
145
146        self.url_root = url_root
147        self.url_opener = urllib2.build_opener(digest_handler, urllib2.HTTPErrorProcessor())
148
149    def _make_url(self, endpoint):
150        return "%s/%s/" % (self.url_root.rstrip("/"), endpoint.strip("/"))
151
152    def post(self, endpoint, data=None, headers=None, timeout_secs=_TIMEOUT_SECS):
153        """
154        Sends a POST request to the specified endpoint with the supplied
155        data.
156
157        Returns the response, either as a string or a JSON object based
158        on the content type.
159        """
160
161        data = utils.default_if_none(data, [])
162        data = json.dumps(data, encoding="utf-8")
163
164        headers = utils.default_if_none(headers, {})
165        headers["Content-Type"] = "application/json; charset=utf-8"
166
167        url = self._make_url(endpoint)
168        request = urllib2.Request(url=url, data=data, headers=headers)
169
170        response = self.url_opener.open(request, timeout=timeout_secs)
171        headers = response.info()
172
173        content_type = headers.gettype()
174        if content_type == "application/json":
175            encoding = headers.getparam("charset") or "utf-8"
176            return json.load(response, encoding=encoding)
177
178        return response.read()
179