diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 09eaef601..f431307a5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,10 +16,10 @@ jobs: python-version: 3.9 steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Set up python - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} diff --git a/Dockerfile b/Dockerfile index 8fd4a933a..c57511b49 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM python:alpine # Get latest root certificates -RUN apk add --no-cache ca-certificates && update-ca-certificates +RUN apk add --no-cache ca-certificates tzdata && update-ca-certificates # Install the required packages RUN pip install --no-cache-dir redis flower @@ -29,4 +29,4 @@ USER flower VOLUME $FLOWER_DATA_DIR -ENTRYPOINT ["flower"] +CMD ["celery", "flower"] diff --git a/README.rst b/README.rst index 7d78a9084..ec323b5e3 100644 --- a/README.rst +++ b/README.rst @@ -73,7 +73,7 @@ Core Celery args that you may want to set:: -b, --broker --result-backend -More info on available `Celery command args `_. +More info on available `Celery command args `_. For Flower command args `see here `_. @@ -104,8 +104,7 @@ Celery command and before Flower sub-command): :: API --- -Flower API enables to manage the cluster via REST API, call tasks and -receive task events in real-time via WebSockets. +Flower API enables to manage the cluster via REST API. For example you can restart worker's pool by: :: @@ -119,13 +118,6 @@ Or terminate executing task by: :: $ curl -X POST -d 'terminate=True' http://localhost:5555/api/task/revoke/8a4da87b-e12b-4547-b89a-e92e4d1f8efd -Or receive task completion events in real-time: :: - - var ws = new WebSocket("ws://localhost:5555/api/task/events/task-succeeded/"); - ws.onmessage = function (event) { - console.log(event.data); - } - For more info checkout `API Reference`_ and `examples`_. .. _API Reference: https://flower.readthedocs.io/en/latest/api.html diff --git a/docker-compose.yml b/docker-compose.yml index 26ab86a0c..d2f4eb329 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,7 @@ services: - redis flower: build: ./ - command: python -m flower -A tasks + command: celery -A tasks flower volumes: - ./examples:/data working_dir: /data @@ -40,3 +40,6 @@ services: environment: CELERY_BROKER_URL: redis://redis CELERY_RESULT_BACKEND: redis://redis + depends_on: + - worker + - redis \ No newline at end of file diff --git a/docs/api.ipynb b/docs/api.ipynb index 76974d70b..daf5a2bc7 100644 --- a/docs/api.ipynb +++ b/docs/api.ipynb @@ -75,7 +75,7 @@ "metadata": {}, "source": [ "# Tasks API\n", - "The tasks API is *async*, meaning calls will return immediatly and you'll need to poll on task status." + "The tasks API is *async*, meaning calls will return immediately and you'll need to poll on task status." ] }, { @@ -195,7 +195,7 @@ "metadata": {}, "source": [ "## result\n", - "Gets the task result. This is *async* and will return immediatly even if the task didn't finish (with state 'PENDING')" + "Gets the task result. This is *async* and will return immediately even if the task didn't finish (with state 'PENDING')" ] }, { @@ -283,7 +283,7 @@ "metadata": {}, "source": [ "## rate-limit\n", - "Update [rate limit](http://docs.celeryproject.org/en/latest/userguide/tasks.html#Task.rate_limit) for a task." + "Update [rate limit](https://docs.celeryq.dev/en/latest/userguide/tasks.html#Task.rate_limit) for a task." ] }, { @@ -324,7 +324,7 @@ "metadata": {}, "source": [ "## timeout\n", - "Set timeout (both [hard](http://docs.celeryproject.org/en/latest/userguide/tasks.html#Task.time_limit) and [soft](http://docs.celeryproject.org/en/latest/userguide/tasks.html#Task.soft_time_limit)) for a task." + "Set timeout (both [hard](https://docs.celeryq.dev/en/latest/userguide/tasks.html#Task.time_limit) and [soft](https://docs.celeryq.dev/en/latest/userguide/tasks.html#Task.soft_time_limit)) for a task." ] }, { @@ -470,7 +470,7 @@ "metadata": {}, "source": [ "## pool/restart\n", - "Restart a worker pool, you need to have [CELERYD_POOL_RESTARTS](http://docs.celeryproject.org/en/latest/configuration.html#std:setting-CELERYD_POOL_RESTARTS) enabled in your configuration)." + "Restart a worker pool, you need to have [CELERYD_POOL_RESTARTS](https://docs.celeryq.dev/en/latest/configuration.html#std:setting-CELERYD_POOL_RESTARTS) enabled in your configuration)." ] }, { @@ -591,7 +591,7 @@ "metadata": {}, "source": [ "## pool/autoscale\n", - "[Autoscale](http://docs.celeryproject.org/en/latest/userguide/workers.html#autoscaling) a pool." + "[Autoscale](https://docs.celeryq.dev/en/latest/userguide/workers.html#autoscaling) a pool." ] }, { @@ -631,7 +631,7 @@ "metadata": {}, "source": [ "## queue/add-consumer\n", - "[Add a consumer](http://docs.celeryproject.org/en/latest/userguide/workers.html#std:control-add_consumer) to a queue." + "[Add a consumer](https://docs.celeryq.dev/en/latest/userguide/workers.html#std:control-add_consumer) to a queue." ] }, { @@ -672,7 +672,7 @@ "metadata": {}, "source": [ "## queue/cancel-consumer\n", - "[Cancel a consumer](http://docs.celeryproject.org/en/latest/userguide/workers.html#queues-cancelling-consumers) queue." + "[Cancel a consumer](https://docs.celeryq.dev/en/latest/userguide/workers.html#queues-cancelling-consumers) queue." ] }, { diff --git a/docs/auth.rst b/docs/auth.rst index cef2211a8..e43ddbe71 100644 --- a/docs/auth.rst +++ b/docs/auth.rst @@ -6,6 +6,11 @@ Protecting your Flower instance from unwarranted access is important if it runs in an untrusted environment. Below, we outline the various forms of authentication supported by Flower. +**NOTE:** The following endpoints are exempt from authentication: + +- /healthcheck +- /metrics + .. _basic-auth: HTTP Basic Authentication diff --git a/docs/config.rst b/docs/config.rst index 1ae5e0b27..61e8e77ac 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -35,7 +35,7 @@ Standard Celery configuration settings can be overridden in the configuration file. See `Celery Configuration reference`_ for a complete listing of all the available settings, and their default values. -.. _`Celery Configuration reference`: http://docs.celeryproject.org/en/latest/userguide/configuration.html +.. _`Celery Configuration reference`: https://docs.celeryq.dev/en/latest/userguide/configuration.html Celery command line options also can be passed to Flower. For example the `--broker` sets the default broker URL: :: @@ -316,3 +316,21 @@ purge_offline_workers Time (in seconds) after which offline workers are automatically removed from dashboard. If omitted, offline workers remain on the dashboard. + +.. _task_runtime_metric_buckets: + +task_runtime_metric_buckets +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sets task runtime latency buckets + +buckets value can be provided as cli options: :: + + $ celery flower --task_runtime_metric_buckets=1,5,10,inf + +Or, it can be also provided as ENV variable: :: + + $ export FLOWER_TASK_RUNTIME_METRIC_BUCKETS=1,5,10,inf + +If not provided: + - default prometheus buckets will be used diff --git a/docs/docker.rst b/docs/docker.rst index b2af2a7e6..a72215294 100644 --- a/docs/docker.rst +++ b/docs/docker.rst @@ -7,7 +7,7 @@ official Redis image. $ docker run --name localredis -p 6379:6379 --rm -d redis -Now, clone this repository, build flower from the Dockerfile, start the +Now, clone this repository (https://github.com/mher/flower), build flower from the Dockerfile, start the container and open http://localhost:49555 :: $ docker build -t "flower" . diff --git a/docs/index.rst b/docs/index.rst index 9c8acb27e..966700674 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,7 +4,7 @@ Flower - Celery monitoring tool Flower is a web based tool for monitoring and administrating `Celery`_ clusters -.. _Celery: https://docs.celeryproject.org/en/stable/# +.. _Celery: https://docs.celeryq.dev/en/stable/# .. include:: features.rst diff --git a/docs/install.rst b/docs/install.rst index 84c82bf6b..5f3c1d255 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -28,7 +28,7 @@ Core Celery args that you may want to set:: -b, --broker --result-backend -More info on available `Celery command args `_. +More info on available `Celery command args `_. For Flower command args `see here `_. @@ -41,7 +41,7 @@ Launch the Flower server at specified port other than default 5555 (open the UI Specify Celery application path with address and port for Flower: :: - $ celery -A proj flower --address=127.0.0.6 --port=5566 + $ celery -A proj flower --address=127.0.0.6 --port=5566 Launch using docker: :: diff --git a/docs/man.rst b/docs/man.rst index 28546baec..eb1020db4 100644 --- a/docs/man.rst +++ b/docs/man.rst @@ -82,6 +82,7 @@ OPTIONS --url_prefix base url prefix --xheaders enable support for the 'X-Real-Ip' and 'X-Scheme' headers. (default *False*) + --task_runtime_metric_buckets task runtime prometheus latency metric buckets (default prometheus latency buckets) TORNADO OPTIONS =============== diff --git a/docs/prometheus-integration.rst b/docs/prometheus-integration.rst index 3f9576128..96e3a9590 100644 --- a/docs/prometheus-integration.rst +++ b/docs/prometheus-integration.rst @@ -77,7 +77,7 @@ We have the following labels available: * **task** - task name, i.e. ``tasks.add``, ``tasks.multiply``. * **type** - task event type, i.e. ``task-started``, ``task-succeeded``. Note that worker related events **will not be counted**. - For more info on task event types see: `task events in celery `_. + For more info on task event types see: `task events in celery `_. * **worker** - celery worker name, i.e ``celery@``. Example Prometheus Alerts @@ -144,7 +144,7 @@ Start Flower Monitoring In your Celery application folder run this command (Flower needs to be installed):: - celery flower -A tasks --broker=redis://localhost:6379/0 + celery -A tasks --broker=redis://localhost:6379/0 flower Configure and Start Prometheus ------------------------------ diff --git a/examples/event-api.html b/examples/event-api.html deleted file mode 100644 index 45225bba2..000000000 --- a/examples/event-api.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - -Flower event API example - - - -

Flower event API

-

This example shows how to get task events

-
- diff --git a/flower/api/__init__.py b/flower/api/__init__.py index b9951b866..de54655c9 100644 --- a/flower/api/__init__.py +++ b/flower/api/__init__.py @@ -1,25 +1,7 @@ -import tornado.websocket +import tornado.web +from ..views import BaseHandler - -class BaseWebSocketHandler(tornado.websocket.WebSocketHandler): - # listeners = [], should be created in derived class - - def open(self): - listeners = self.listeners - listeners.append(self) - - def on_message(self, message): - pass - - def on_close(self): - listeners = self.listeners - if self in listeners: - listeners.remove(self) - - @classmethod - def send_message(cls, message): - for l in cls.listeners: - l.write_message(message) - - def check_origin(self, origin): - return True +class BaseApiHandler(BaseHandler): + def prepare(self): + if self.application.options.basic_auth or self.application.options.auth: + raise tornado.web.HTTPError(405, "api is not available when auth is enabled") diff --git a/flower/api/control.py b/flower/api/control.py index 363708b4f..f86ac7d37 100644 --- a/flower/api/control.py +++ b/flower/api/control.py @@ -7,13 +7,13 @@ from tornado import gen from tornado import util -from ..views import BaseHandler +from . import BaseApiHandler logger = logging.getLogger(__name__) -class ControlHandler(BaseHandler): +class ControlHandler(BaseApiHandler): def is_worker(self, workername): return workername and workername in self.application.workers diff --git a/flower/api/events.py b/flower/api/events.py deleted file mode 100644 index 7ab9d3a31..000000000 --- a/flower/api/events.py +++ /dev/null @@ -1,35 +0,0 @@ -import sys - -from ..api import BaseWebSocketHandler - - -class EventsApiHandler(BaseWebSocketHandler): - def open(self, task_id=None): - BaseWebSocketHandler.open(self) - self.task_id = task_id - - @classmethod - def send_message(cls, event): - for l in cls.listeners: - if not l.task_id or l.task_id == event['uuid']: - l.write_message(event) - - -EVENTS = ('task-sent', 'task-received', 'task-started', 'task-succeeded', - 'task-failed', 'task-revoked', 'task-retried', 'task-custom') - - -def getClassName(eventname): - return ''.join(map(lambda x: x[0].upper() + x[1:], eventname.split('-'))) - - -# Dynamically generates handler classes -thismodule = sys.modules[__name__] -for event in EVENTS: - classname = getClassName(event) - setattr(thismodule, classname, - type(classname, (EventsApiHandler, ), {'listeners': []})) - - -__all__ = list(map(getClassName, EVENTS)) -__all__.append(getClassName) diff --git a/flower/api/tasks.py b/flower/api/tasks.py index 54b682271..85c3f358c 100644 --- a/flower/api/tasks.py +++ b/flower/api/tasks.py @@ -15,7 +15,7 @@ from celery.backends.base import DisabledBackend from ..utils import tasks -from ..views import BaseHandler +from . import BaseApiHandler from ..utils.broker import Broker from ..api.control import ControlHandler from collections import OrderedDict @@ -24,7 +24,7 @@ logger = logging.getLogger(__name__) -class BaseTaskHandler(BaseHandler): +class BaseTaskHandler(BaseApiHandler): DATE_FORMAT = '%Y-%m-%d %H:%M:%S.%f' def get_task_args(self): diff --git a/flower/command.py b/flower/command.py index ed5833c51..6a64010cd 100644 --- a/flower/command.py +++ b/flower/command.py @@ -149,9 +149,15 @@ def is_flower_envvar(name): def print_banner(app, ssl): if not options.unix_socket: + if options.url_prefix: + prefix_str = f'/{options.url_prefix}/' + else: + prefix_str = '' + logger.info( - "Visit me at http%s://%s:%s", 's' if ssl else '', - options.address or 'localhost', options.port + "Visit me at http%s://%s:%s%s", 's' if ssl else '', + options.address or 'localhost', options.port, + prefix_str ) else: logger.info("Visit me via unix socket file: %s", options.unix_socket) diff --git a/flower/events.py b/flower/events.py index 38bc98f1f..2268f2fd2 100644 --- a/flower/events.py +++ b/flower/events.py @@ -1,3 +1,4 @@ +import os import time import shelve import logging @@ -11,6 +12,7 @@ from celery.events import EventReceiver from celery.events.state import State +from tornado.options import options from . import api @@ -20,26 +22,44 @@ logger = logging.getLogger(__name__) +prometheus_metrics = None + + +def get_prometheus_metrics(): + global prometheus_metrics + if prometheus_metrics is None: + prometheus_metrics = PrometheusMetrics() + + return prometheus_metrics + class PrometheusMetrics(object): - events = PrometheusCounter('flower_events_total', "Number of events", ['worker', 'type', 'task']) - runtime = Histogram('flower_task_runtime_seconds', "Task runtime", ['worker', 'task']) - prefetch_time = Gauge( - 'flower_task_prefetch_time_seconds', - "The time the task spent waiting at the celery worker to be executed.", - ['worker', 'task'] - ) - number_of_prefetched_tasks = Gauge( - 'flower_worker_prefetched_tasks', - 'Number of tasks of given type prefetched at a worker', - ['worker', 'task'] - ) - worker_online = Gauge('flower_worker_online', "Worker online status", ['worker']) - worker_number_of_currently_executing_tasks = Gauge( - 'flower_worker_number_of_currently_executing_tasks', - "Number of tasks currently executing at a worker", - ['worker'] - ) + + def __init__(self): + self.events = PrometheusCounter('flower_events_total', "Number of events", ['worker', 'type', 'task']) + + self.runtime = Histogram( + 'flower_task_runtime_seconds', + "Task runtime", + ['worker', 'task'], + buckets=options.task_runtime_metric_buckets + ) + self.prefetch_time = Gauge( + 'flower_task_prefetch_time_seconds', + "The time the task spent waiting at the celery worker to be executed.", + ['worker', 'task'] + ) + self.number_of_prefetched_tasks = Gauge( + 'flower_worker_prefetched_tasks', + 'Number of tasks of given type prefetched at a worker', + ['worker', 'task'] + ) + self.worker_online = Gauge('flower_worker_online', "Worker online status", ['worker']) + self.worker_number_of_currently_executing_tasks = Gauge( + 'flower_worker_number_of_currently_executing_tasks', + "Number of tasks currently executing at a worker", + ['worker'] + ) class EventsState(State): @@ -48,7 +68,7 @@ class EventsState(State): def __init__(self, *args, **kwargs): super(EventsState, self).__init__(*args, **kwargs) self.counter = collections.defaultdict(Counter) - self.metrics = PrometheusMetrics() + self.metrics = get_prometheus_metrics() def event(self, event): # Save the event @@ -97,11 +117,6 @@ def event(self, event): if event_type == 'worker-offline': self.metrics.worker_online.labels(worker_name).set(0) - # Send event to api subscribers (via websockets) - classname = api.events.getClassName(event_type) - cls = getattr(api.events, classname, None) - if cls: - cls.send_message(event) class Events(threading.Thread): diff --git a/flower/options.py b/flower/options.py index 45e096ab2..f69ffecb0 100644 --- a/flower/options.py +++ b/flower/options.py @@ -1,5 +1,6 @@ import types +from prometheus_client import Histogram from tornado.options import define from tornado.options import options @@ -67,6 +68,8 @@ define("auth_provider", default='flower.views.auth.GoogleAuth2LoginHandler', help="auth handler class") define("url_prefix", type=str, help="base url prefix") +define("task_runtime_metric_buckets", type=float, default=Histogram.DEFAULT_BUCKETS, + multiple=True, help="histogram latency bucket value") # deprecated options define("inspect", default=False, help="inspect workers", type=bool) diff --git a/flower/static/js/flower.js b/flower/static/js/flower.js index b19b029c6..165af9176 100644 --- a/flower/static/js/flower.js +++ b/flower/static/js/flower.js @@ -468,7 +468,7 @@ var flower = (function () { data: 'hostname', type: 'natural', render: function (data, type, full, meta) { - return '' + data + ''; + return '' + data + ''; } }, { targets: 1, @@ -541,7 +541,7 @@ var flower = (function () { url: url_prefix() + '/tasks/datatable' }, order: [ - [7, "asc"] + [7, "desc"] ], oSearch: { "sSearch": $.urlParam('state') ? 'state:' + $.urlParam('state') : '' @@ -559,7 +559,7 @@ var flower = (function () { visible: isColumnVisible('uuid'), orderable: false, render: function (data, type, full, meta) { - return '' + data + ''; + return '' + data + ''; } }, { targets: 2, @@ -623,7 +623,7 @@ var flower = (function () { data: 'worker', visible: isColumnVisible('worker'), render: function (data, type, full, meta) { - return '' + data + ''; + return '' + data + ''; } }, { targets: 10, diff --git a/flower/templates/base.html b/flower/templates/base.html index 1d5b5afd5..70871ba36 100644 --- a/flower/templates/base.html +++ b/flower/templates/base.html @@ -8,7 +8,7 @@ - + diff --git a/flower/templates/task.html b/flower/templates/task.html index bcb141492..b01ef6b69 100644 --- a/flower/templates/task.html +++ b/flower/templates/task.html @@ -79,6 +79,11 @@

{{ getattr(task, 'name', None) }} {% elif name in ['parent_id', 'root_id'] %} {{ getattr(task, name, None) }} + {% elif name == 'children' %} + {% for child in getattr(task, name, {}) %} + {{ child.id }} +
+ {% end %} {% else %} {{ getattr(task, name, None) }} {% end %} diff --git a/flower/templates/worker.html b/flower/templates/worker.html index ecf2b8114..5c419517c 100644 --- a/flower/templates/worker.html +++ b/flower/templates/worker.html @@ -336,7 +336,7 @@

Task limits

{% for name,value in sorted(worker.get('conf', {}).items()) %} {% if value is not None %} - {{ name }} + {{ name }} {{ value }} {% end %} diff --git a/flower/urls.py b/flower/urls.py index 2d64cc0b4..7d6332f8b 100644 --- a/flower/urls.py +++ b/flower/urls.py @@ -2,7 +2,6 @@ from tornado.web import StaticFileHandler, url -from .api import events from .api import control from .api import tasks from .api import workers @@ -57,15 +56,6 @@ (r"/api/task/timeout/(.+)", control.TaskTimout), (r"/api/task/rate-limit/(.+)", control.TaskRateLimit), (r"/api/task/revoke/(.+)", control.TaskRevoke), - # Events WebSocket API - (r"/api/task/events/task-sent/(.*)", events.TaskSent), - (r"/api/task/events/task-received/(.*)", events.TaskReceived), - (r"/api/task/events/task-started/(.*)", events.TaskStarted), - (r"/api/task/events/task-succeeded/(.*)", events.TaskSucceeded), - (r"/api/task/events/task-failed/(.*)", events.TaskFailed), - (r"/api/task/events/task-revoked/(.*)", events.TaskRevoked), - (r"/api/task/events/task-retried/(.*)", events.TaskRetried), - (r"/api/task/events/task-custom/(.*)", events.TaskCustom), # Metrics (r"/metrics", monitor.Metrics), (r"/healthcheck", monitor.Healthcheck), diff --git a/flower/utils/__init__.py b/flower/utils/__init__.py index 45289d511..13f183cc4 100644 --- a/flower/utils/__init__.py +++ b/flower/utils/__init__.py @@ -20,11 +20,11 @@ def bugreport(app=None): return 'flower -> flower:%s tornado:%s humanize:%s%s' % ( __version__, tornado.version, - humanize.VERSION, + getattr(humanize, '__version__', None) or getattr(humanize, 'VERSION'), app.bugreport() ) - except (ImportError, AttributeError): - return 'Unknown Celery version' + except (ImportError, AttributeError) as e: + return f"Error when generating bug report: {e}. Have you installed correct versions of Flower's dependencies?" def abs_path(path): diff --git a/flower/utils/broker.py b/flower/utils/broker.py index cc008652c..c255962a5 100644 --- a/flower/utils/broker.py +++ b/flower/utils/broker.py @@ -168,7 +168,7 @@ def __init__(self, broker_url, *args, **kwargs): self.port = self.port or 26379 self.vhost = self._prepare_virtual_host(self.vhost) self.master_name = self._prepare_master_name(broker_options) - self.redis = self._get_redis_client() + self.redis = self._get_redis_client(broker_options) def _prepare_virtual_host(self, vhost): if not isinstance(vhost, numbers.Integral): @@ -194,8 +194,11 @@ def _prepare_master_name(self, broker_options): ) return master_name - def _get_redis_client(self): - connection_kwargs = {'password': self.password} + def _get_redis_client(self, broker_options): + connection_kwargs = { + 'password': self.password, + 'sentinel_kwargs': broker_options.get('sentinel_kwargs') + } # TODO: get all sentinel hosts from Celery App config and use them to initialize Sentinel sentinel = redis.sentinel.Sentinel( [(self.host, self.port)], **connection_kwargs) diff --git a/flower/views/__init__.py b/flower/views/__init__.py index 84245f3d1..ffef204b3 100644 --- a/flower/views/__init__.py +++ b/flower/views/__init__.py @@ -17,11 +17,12 @@ class BaseHandler(tornado.web.RequestHandler): def set_default_headers(self): self.set_header("Access-Control-Allow-Origin", "*") - self.set_header("Access-Control-Allow-Headers", "x-requested-with") + self.set_header("Access-Control-Allow-Headers", + "x-requested-with,access-control-allow-origin,authorization,content-type") self.set_header('Access-Control-Allow-Methods', - ' PUT, DELETE, OPTIONS') + ' PUT, DELETE, OPTIONS, POST, GET, PATCH') - def options(self): + def options(self, *args, **kwargs): self.set_status(204) self.finish() diff --git a/flower/views/monitor.py b/flower/views/monitor.py index 6a99f92cc..302cc8589 100644 --- a/flower/views/monitor.py +++ b/flower/views/monitor.py @@ -1,20 +1,18 @@ import prometheus_client -from tornado import web from tornado import gen from ..views import BaseHandler class Metrics(BaseHandler): - @web.authenticated @gen.coroutine def get(self): self.write(prometheus_client.generate_latest()) self.set_header("Content-Type", "text/plain") + class Healthcheck(BaseHandler): @gen.coroutine def get(self): self.write("OK") - \ No newline at end of file diff --git a/tests/unit/api/test_control.py b/tests/unit/api/test_control.py index 31819f3d3..a83d9ad6a 100644 --- a/tests/unit/api/test_control.py +++ b/tests/unit/api/test_control.py @@ -1,8 +1,9 @@ from mock import MagicMock - +import mock from flower.api.control import ControlHandler from tests.unit import AsyncHTTPTestCase +from tornado.options import options class UnknownWorkerControlTests(AsyncHTTPTestCase): @@ -169,3 +170,12 @@ def test_terminate_signal(self): celery.control.revoke.assert_called_once_with('test', terminate=True, signal='SIGUSR1') + + +class ControlAuthTests(WorkerControlTests): + def test_auth(self): + with mock.patch.object(options.mockable(), 'basic_auth', ['user1:password1']): + app = self._app.capp + app.control.broadcast = MagicMock() + r = self.post('/api/worker/shutdown/test', body={}) + self.assertEqual(405, r.code) diff --git a/tests/unit/api/test_events.py b/tests/unit/api/test_events.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/unit/test_command.py b/tests/unit/test_command.py index 117741f1c..4e90e63ae 100644 --- a/tests/unit/test_command.py +++ b/tests/unit/test_command.py @@ -6,13 +6,31 @@ from unittest.mock import Mock, patch import mock +from prometheus_client import Histogram -from flower.command import apply_options, warn_about_celery_args_used_in_flower_command +from flower.command import apply_options, warn_about_celery_args_used_in_flower_command, apply_env_options from tornado.options import options from tests.unit import AsyncHTTPTestCase class TestFlowerCommand(AsyncHTTPTestCase): + def test_task_runtime_metric_buckets_read_from_cmd_line(self): + apply_options('flower', argv=['--task_runtime_metric_buckets=1,10,inf']) + self.assertEqual([1.0, 10.0, float('inf')], options.task_runtime_metric_buckets) + + def test_task_runtime_metric_buckets_no_cmd_line_arg(self): + apply_options('flower', argv=[]) + self.assertEqual(Histogram.DEFAULT_BUCKETS, options.task_runtime_metric_buckets) + + def test_task_runtime_metric_buckets_read_from_env(self): + os.environ["FLOWER_TASK_RUNTIME_METRIC_BUCKETS"] = "2,5,inf" + apply_env_options() + self.assertEqual([2.0, 5.0, float('inf')], options.task_runtime_metric_buckets) + + def test_task_runtime_metric_buckets_no_env_value_provided(self): + apply_env_options() + self.assertEqual(Histogram.DEFAULT_BUCKETS, options.task_runtime_metric_buckets) + def test_port(self): with self.mock_option('port', 5555): apply_options('flower', argv=['--port=123']) diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index a0032619f..4e4fada45 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -1,4 +1,5 @@ import unittest +from unittest.mock import Mock from flower.utils import bugreport from celery import Celery @@ -7,7 +8,7 @@ class BugreportTests(unittest.TestCase): def test_default(self): report = bugreport() - self.assertFalse('Unknown Celery version' in report) + self.assertFalse('Error when generating bug report' in report) self.assertTrue('tornado' in report) self.assertTrue('humanize' in report) self.assertTrue('celery' in report) @@ -15,7 +16,15 @@ def test_default(self): def test_with_app(self): app = Celery() report = bugreport(app) - self.assertFalse('Unknown Celery version' in report) + self.assertFalse('Error when generating bug report' in report) self.assertTrue('tornado' in report) self.assertTrue('humanize' in report) self.assertTrue('celery' in report) + + def test_when_unable_to_generate_report(self): + fake_app = Mock() + fake_app.bugreport.side_effect = ImportError('import error message') + report = bugreport(fake_app) + self.assertTrue('Error when generating bug report' in report) + self.assertTrue('import error message' in report) + self.assertTrue("Have you installed correct versions of Flower's dependencies?" in report) diff --git a/tests/unit/views/test_monitor.py b/tests/unit/views/test_monitor.py index b2041a700..240f55aa9 100644 --- a/tests/unit/views/test_monitor.py +++ b/tests/unit/views/test_monitor.py @@ -208,4 +208,4 @@ def get_app(self): def test_healthcheck_route(self): response = self.get('/healthcheck').body.decode('utf-8') - self.assertEquals(response, 'OK') + self.assertEqual(response, 'OK')