|
@@ -1,9 +1,24 @@
|
|
|
+"""
|
|
|
+
|
|
|
+ Publishing Statistics and Monitoring Celery.
|
|
|
+
|
|
|
+"""
|
|
|
from carrot.connection import DjangoAMQPConnection
|
|
|
from celery.messaging import StatsPublisher, StatsConsumer
|
|
|
from django.conf import settings
|
|
|
+import time
|
|
|
|
|
|
|
|
|
class Statistics(object):
|
|
|
+ """Base class for classes publishing celery statistics.
|
|
|
+
|
|
|
+ .. attribute:: type
|
|
|
+
|
|
|
+ **REQUIRED** The type of statistics this class handles.
|
|
|
+
|
|
|
+ **Required handlers**
|
|
|
+
|
|
|
+ """
|
|
|
type = None
|
|
|
|
|
|
def __init__(self, **kwargs):
|
|
@@ -13,6 +28,13 @@ class Statistics(object):
|
|
|
"Statistic classes must define their type.")
|
|
|
|
|
|
def publish(self, **data):
|
|
|
+ """Publish statistics to be collected later by
|
|
|
+ :class:`StatsCollector`.
|
|
|
+
|
|
|
+ :param data: An arbitrary Python object containing the statistics
|
|
|
+ to be published.
|
|
|
+
|
|
|
+ """
|
|
|
if not self.enabled:
|
|
|
return
|
|
|
connection = DjangoAMQPConnection()
|
|
@@ -23,49 +45,117 @@ class Statistics(object):
|
|
|
|
|
|
@classmethod
|
|
|
def start(cls, *args, **kwargs):
|
|
|
+ """Convenience method instantiating and running :meth:`run` in
|
|
|
+ one swoop."""
|
|
|
stat = cls()
|
|
|
- stat.run()
|
|
|
+ stat.run(*args, **kwargs)
|
|
|
return stat
|
|
|
|
|
|
def run(self, *args, **kwargs):
|
|
|
- if stat.enabled:
|
|
|
- stat.on_start(*args, **kwargs)
|
|
|
+ """Start producing statistics."""
|
|
|
+ if self.enabled:
|
|
|
+ return self.on_start(*args, **kwargs)
|
|
|
|
|
|
def stop(self, *args, **kwargs):
|
|
|
+ """Stop producing and publish statistics."""
|
|
|
if self.enabled:
|
|
|
- self.on_finish(*args, **kwargs)
|
|
|
+ return self.on_finish(*args, **kwargs)
|
|
|
+
|
|
|
+ def on_start(self, *args, **kwargs):
|
|
|
+ """What to do when the :meth:`run` method is called."""
|
|
|
+ raise NotImplementedError(
|
|
|
+ "Statistics classes must define a on_start handler.")
|
|
|
|
|
|
+ def on_stop(self, *args, **kwargs):
|
|
|
+ """What to do when the :meth:`stop` method is called."""
|
|
|
+ raise NotImplementedError(
|
|
|
+ "Statistics classes must define a on_stop handler.")
|
|
|
|
|
|
class TimerStats(Statistics):
|
|
|
+ """A generic timer producing ``celery`` statistics.
|
|
|
+
|
|
|
+ .. attribute:: time_start
|
|
|
+
|
|
|
+ The time when this class was instantiated (in :func:`time.time`
|
|
|
+ format.)
|
|
|
+
|
|
|
+ """
|
|
|
time_start = None
|
|
|
|
|
|
def on_start(self, task_id, task_name, args, kwargs):
|
|
|
+ """What to do when the timers :meth:`run` method is called."""
|
|
|
self.task_id = task_id
|
|
|
self.task_name = task_name
|
|
|
- self.args = self.args
|
|
|
- self.kwargs = self.kwargs
|
|
|
+ self.args = args
|
|
|
+ self.kwargs = kwargs
|
|
|
self.time_start = time.time()
|
|
|
|
|
|
def on_finish(self):
|
|
|
+ """What to do when the timers :meth:`stop` method is called.
|
|
|
+
|
|
|
+ :returns: the time in seconds it took between calling :meth:`start` on
|
|
|
+ this class and :meth:`stop`.
|
|
|
+ """
|
|
|
nsecs = time.time() - self.time_start
|
|
|
- self.publish(task_id=task_id,
|
|
|
- task_name=task_name,
|
|
|
- args=args,
|
|
|
- kwargs=kwargs,
|
|
|
- nsecs=nsecs)
|
|
|
+ self.publish(task_id=self.task_id,
|
|
|
+ task_name=self.task_name,
|
|
|
+ args=self.args,
|
|
|
+ kwargs=self.kwargs,
|
|
|
+ nsecs=str(nsecs))
|
|
|
+ return nsecs
|
|
|
|
|
|
|
|
|
class TaskTimerStats(TimerStats):
|
|
|
+ """Time a running :class:`celery.task.Task`."""
|
|
|
type = "task_time_running"
|
|
|
|
|
|
|
|
|
class StatsCollector(object):
|
|
|
+ """Collect and report Celery statistics.
|
|
|
+
|
|
|
+ **NOTE**: Please run only one collector at any time, or your stats
|
|
|
+ will be skewed.
|
|
|
+
|
|
|
+ .. attribute:: total_tasks_processed
|
|
|
+
|
|
|
+ The number of tasks executed in total since the first time
|
|
|
+ :meth:`collect` was executed on this class instance.
|
|
|
+
|
|
|
+ .. attribute:: total_tasks_processed_by_type
|
|
|
+
|
|
|
+ A dictionary of task names and how many times they have been
|
|
|
+ executed in total since the first time :meth:`collect` was executed
|
|
|
+ on this class instance.
|
|
|
+
|
|
|
+ .. attribute:: total_task_time_running
|
|
|
+
|
|
|
+ The total time, in seconds, it took to process all the tasks executed
|
|
|
+ since the first time :meth:`collect` was executed on this class
|
|
|
+ instance.
|
|
|
+
|
|
|
+ .. attribute:: total_task_time_running_by_type
|
|
|
+
|
|
|
+ A dictionary of task names and their total running time in seconds,
|
|
|
+ counting all the tasks that has been run since the first time
|
|
|
+ :meth:`collect` was executed on this class instance.
|
|
|
+
|
|
|
+ **NOTE**: You have to run :meth:`collect` for these attributes
|
|
|
+ to be filled.
|
|
|
+
|
|
|
+
|
|
|
+ """
|
|
|
+
|
|
|
allowed_types = ["task_time_running"]
|
|
|
- total_tasks_processed = 0
|
|
|
- total_task_time_running = 0
|
|
|
- total_task_time_running_by_type = {}
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ self.total_tasks_processed = 0
|
|
|
+ self.total_tasks_processed_by_type = {}
|
|
|
+ self.total_task_time_running = 0.0
|
|
|
+ self.total_task_time_running_by_type = {}
|
|
|
|
|
|
def collect(self):
|
|
|
+ """Collect any new statistics available since the last time
|
|
|
+ :methd:`collect` was executed."""
|
|
|
connection = DjangoAMQPConnection()
|
|
|
consumer = StatsConsumer(connection=connection)
|
|
|
it = consumer.iterqueue(infinite=False)
|
|
@@ -75,21 +165,51 @@ class StatsCollector(object):
|
|
|
if stat_type in self.allowed_types:
|
|
|
handler = getattr(self, stat_type)
|
|
|
handler(**stats_entry["data"])
|
|
|
- return self.on_cycle_end()
|
|
|
|
|
|
def task_time_running(self, task_id, task_name, args, kwargs, nsecs):
|
|
|
+ """Process statistics regarding how long a task has been running
|
|
|
+ (the :class:TaskTimerStats` class is responsible for sending these).
|
|
|
+
|
|
|
+ :param task_id: The UUID of the task.
|
|
|
+ :param task_name: The name of task.
|
|
|
+ :param args: The tasks positional arguments.
|
|
|
+ :param kwargs: The tasks keyword arguments.
|
|
|
+ :param nsecs: The number of seconds (in :func:`time.time` format)
|
|
|
+ it took to execute the task.
|
|
|
+
|
|
|
+ """
|
|
|
+ nsecs = float(nsecs)
|
|
|
+ self.total_tasks_processed += 1
|
|
|
self.total_task_time_running += nsecs
|
|
|
- self.total_task_time_running_by_type[task_name] = \
|
|
|
- self.total_task_time_running_by_type.get(task_name, nsecs)
|
|
|
- self.total_task_time_running_by_type[task_name] += nsecs
|
|
|
- print("Task %s[%s](%s, %s): %d" % (
|
|
|
- task_id, task_name, args, kwargs, nsecs))
|
|
|
+ if task_name not in self.total_task_time_running_by_type:
|
|
|
+ self.total_task_time_running_by_type[task_name] = nsecs
|
|
|
+ else:
|
|
|
+ self.total_task_time_running_by_type[task_name] += nsecs
|
|
|
+ if task_name not in self.total_tasks_processed_by_type:
|
|
|
+ self.total_tasks_processed_by_type[task_name] = 1
|
|
|
+ else:
|
|
|
+ self.total_tasks_processed_by_type[task_name] += 1
|
|
|
+
|
|
|
+ def report(self):
|
|
|
+ """Dump a nice statistics report from the data collected since
|
|
|
+ the first time :meth:`collect` was executed on this instance.
|
|
|
+
|
|
|
+ It outputs the following information:
|
|
|
+
|
|
|
+ * Total processing time by task type and how many times each
|
|
|
+ task has been excuted.
|
|
|
+
|
|
|
+ * Total task processing time.
|
|
|
|
|
|
- def on_cycle_end(self):
|
|
|
+ * Total number of tasks executed.
|
|
|
+
|
|
|
+ """
|
|
|
print("-" * 64)
|
|
|
print("Total processing time by task type:")
|
|
|
for task_name, nsecs in self.total_task_time_running_by_type.items():
|
|
|
- print("\t%s: %d" % (task_name, nsecs))
|
|
|
- print("Total task processing time: %d" % (
|
|
|
+ print("\t%s: %s secs. (for a total of %d executed.)" % (
|
|
|
+ task_name, nsecs,
|
|
|
+ self.total_tasks_processed_by_type.get(task_name)))
|
|
|
+ print("Total task processing time: %s secs." % (
|
|
|
self.total_task_time_running))
|
|
|
print("Total tasks processed: %d" % self.total_tasks_processed)
|