|
6 | 6 |
|
7 | 7 | from __future__ import absolute_import |
8 | 8 |
|
| 9 | +import logging |
| 10 | +try: |
| 11 | + from logging import NullHandler |
| 12 | +except ImportError: |
| 13 | + # Create a NullHandler class in logging for python 2.6 |
| 14 | + class NullHandler(logging.Handler): |
| 15 | + def emit(self, record): |
| 16 | + pass |
| 17 | + |
9 | 18 | import socket |
| 19 | +try: |
| 20 | + from threading import RLock |
| 21 | + from threading import Timer |
| 22 | +except ImportError: |
| 23 | + RLock = None |
| 24 | + Timer = None |
| 25 | +import time |
10 | 26 |
|
11 | 27 | import riemann_client.riemann_pb2 |
12 | 28 | import riemann_client.transport |
13 | 29 |
|
14 | 30 |
|
| 31 | +logger = logging.getLogger(__name__) |
| 32 | +logger.addHandler(NullHandler()) |
| 33 | + |
| 34 | + |
15 | 35 | class Client(object): |
16 | 36 | """A client for sending events and querying a Riemann server. |
17 | 37 |
|
@@ -200,3 +220,147 @@ def send_events(self, events): |
200 | 220 | def clear_queue(self): |
201 | 221 | """Resets the message/queue to a blank :py:class:`.Msg` object""" |
202 | 222 | self.queue = riemann_client.riemann_pb2.Msg() |
| 223 | + |
| 224 | + |
| 225 | +if RLock and Timer: # noqa |
| 226 | + class AutoFlushingQueuedClient(QueuedClient): |
| 227 | + """A Riemann client using a queue and a timer that will automatically |
| 228 | + flush its contents if either: |
| 229 | + - the queue size exceeds :param max_batch_size: or |
| 230 | + - more than :param max_delay: has elapsed since the last flush and |
| 231 | + the queue is non-empty. |
| 232 | +
|
| 233 | + if :param stay_connected: is False, then the transport will be |
| 234 | + disconnected after each flush and reconnected at the beginning of |
| 235 | + the next flush. |
| 236 | + if :param clear_on_fail: is True, then the client will discard its |
| 237 | + buffer after the second retry in the event of a socket error. |
| 238 | +
|
| 239 | + A message object is used as a queue, and the following methods are |
| 240 | + given: |
| 241 | + - :py:meth:`.send_event` - add a new event to the queue |
| 242 | + - :py:meth:`.send_events` add a tuple of new events to the queue |
| 243 | + - :py:meth:`.event` - add a new event to the queue from |
| 244 | + keyword arguments |
| 245 | + - :py:meth:`.events` - add new events to the queue from |
| 246 | + dictionaries |
| 247 | + - :py:meth:`.flush` - manually force flush the queue to the |
| 248 | + transport |
| 249 | + """ |
| 250 | + |
| 251 | + def __init__(self, transport, max_delay=0.5, max_batch_size=100, |
| 252 | + stay_connected=False, clear_on_fail=False): |
| 253 | + super(AutoFlushingQueuedClient, self).__init__(transport) |
| 254 | + self.stay_connected = stay_connected |
| 255 | + self.clear_on_fail = clear_on_fail |
| 256 | + self.max_delay = max_delay |
| 257 | + self.max_batch_size = max_batch_size |
| 258 | + self.lock = RLock() |
| 259 | + self.event_counter = 0 |
| 260 | + self.last_flush = time.time() |
| 261 | + self.timer = None |
| 262 | + |
| 263 | + # start the timer |
| 264 | + self.start_timer() |
| 265 | + |
| 266 | + def connect(self): |
| 267 | + """Connect the transport if it is not already connected.""" |
| 268 | + if not self.is_connected(): |
| 269 | + self.transport.connect() |
| 270 | + |
| 271 | + def is_connected(self): |
| 272 | + """Check whether the transport is connected.""" |
| 273 | + try: |
| 274 | + # this will throw an exception whenever socket isn't connected |
| 275 | + self.transport.socket.type |
| 276 | + return True |
| 277 | + except (AttributeError, RuntimeError, socket.error): |
| 278 | + return False |
| 279 | + |
| 280 | + def event(self, **data): |
| 281 | + """Enqueues an event, using keyword arguments to create an Event |
| 282 | +
|
| 283 | + >>> client.event(service='riemann-client', state='awesome') |
| 284 | +
|
| 285 | + :param \*\*data: keyword arguments used for :py:func:`create_event` |
| 286 | + """ |
| 287 | + self.send_events((self.create_event(data),)) |
| 288 | + |
| 289 | + def events(self, *events): |
| 290 | + """Enqueues multiple events in a single message |
| 291 | +
|
| 292 | + >>> client.events({'service': 'riemann-client', |
| 293 | + >>> 'state': 'awesome'}) |
| 294 | +
|
| 295 | + :param \*events: event dictionaries for :py:func:`create_event` |
| 296 | + :returns: The response message from Riemann |
| 297 | + """ |
| 298 | + self.send_events(self.create_event(evd) for evd in events) |
| 299 | + |
| 300 | + def send_events(self, events): |
| 301 | + """Enqueues multiple events |
| 302 | +
|
| 303 | + :param events: A list or iterable of ``Event`` objects |
| 304 | + :returns: The response message from Riemann |
| 305 | + """ |
| 306 | + with self.lock: |
| 307 | + for event in events: |
| 308 | + self.queue.events.add().MergeFrom(event) |
| 309 | + self.event_counter += 1 |
| 310 | + self.check_for_flush() |
| 311 | + |
| 312 | + def flush(self): |
| 313 | + """Sends the events in the queue to Riemann in a single protobuf |
| 314 | + message |
| 315 | +
|
| 316 | + :returns: The response message from Riemann |
| 317 | + """ |
| 318 | + response = None |
| 319 | + with self.lock: |
| 320 | + if not self.is_connected(): |
| 321 | + self.connect() |
| 322 | + try: |
| 323 | + response = super(AutoFlushingQueuedClient, self).flush() |
| 324 | + except socket.error: |
| 325 | + # log and retry |
| 326 | + logger.warn("Socket error on flushing. " |
| 327 | + "Attempting reconnect and retry...") |
| 328 | + try: |
| 329 | + self.transport.disconnect() |
| 330 | + self.connect() |
| 331 | + response = ( |
| 332 | + super(AutoFlushingQueuedClient, self).flush()) |
| 333 | + except: |
| 334 | + logger.warn("Socket error on flushing " |
| 335 | + "second attempt. Batch discarded.") |
| 336 | + self.transport.disconnect() |
| 337 | + if self.clear_on_fail: |
| 338 | + self.clear_queue() |
| 339 | + self.event_counter = 0 |
| 340 | + if not self.stay_connected: |
| 341 | + self.transport.disconnect() |
| 342 | + self.last_flush = time.time() |
| 343 | + self.start_timer() |
| 344 | + return response |
| 345 | + |
| 346 | + def check_for_flush(self): |
| 347 | + """Checks the conditions for flushing the queue""" |
| 348 | + if (self.event_counter >= self.max_batch_size or |
| 349 | + (time.time() - self.last_flush) >= self.max_delay): |
| 350 | + self.flush() |
| 351 | + |
| 352 | + def start_timer(self): |
| 353 | + """Cycle the timer responsible for periodically flushing the queue |
| 354 | + """ |
| 355 | + if self.timer: |
| 356 | + self.timer.cancel() |
| 357 | + self.timer = Timer(self.max_delay, self.check_for_flush) |
| 358 | + self.timer.daemon = True |
| 359 | + self.timer.start() |
| 360 | + |
| 361 | + def stop_timer(self): |
| 362 | + """Stops the current timer |
| 363 | +
|
| 364 | + a :py:meth:`.flush` event will reactviate the timer |
| 365 | + """ |
| 366 | + self.timer.cancel() |
0 commit comments