diff --git a/docs/config.rst b/docs/config.rst index b1f3a024..7a52e520 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -331,6 +331,16 @@ Enables showing time relative to the page refresh time in a more human-readable When enabled, timestamps will be shown as relative time such as "2 minutes ago" or "1 hour ago" instead of the exact timestamp. + +.. _browser_local_time: + +browser_local_time +~~~~~~~~~~~~~~~~~~ + +Default: False + +Show time in the browser's local timezone. If not specified, or set to `False`, the timezone of the Celery app is used. + .. _persistent: persistent diff --git a/docs/man.rst b/docs/man.rst index 65fa8159..3a6a4f04 100644 --- a/docs/man.rst +++ b/docs/man.rst @@ -55,6 +55,7 @@ OPTIONS basic auth --broker-api inspect broker e.g. http://guest:guest@localhost:15672/api/ + --browser-local-time show time in the browser's local TZ (default *False*) --ca-certs path to SSL certificate authority (CA) file --certfile path to SSL certificate file --conf flower configuration file path (default *flowerconfig.py*) diff --git a/flower/options.py b/flower/options.py index 294f4ee0..53e5037a 100644 --- a/flower/options.py +++ b/flower/options.py @@ -61,6 +61,8 @@ help="use custom task formatter") define("natural_time", type=bool, default=False, help="show time in relative format") +define("browser_local_time", type=bool, default=False, + help="show time in the browser's local TZ") define("tasks_columns", type=str, default="name,uuid,state,args,kwargs,result,received,started,runtime,worker", help="slugs of columns on /tasks/ page, delimited by comma") diff --git a/flower/static/js/flower.js b/flower/static/js/flower.js index 54249944..3a9dfbaf 100644 --- a/flower/static/js/flower.js +++ b/flower/static/js/flower.js @@ -525,14 +525,18 @@ var flower = (function () { } function format_time(timestamp) { - var time = $('#time').val(), - prefix = time.startsWith('natural-time') ? 'natural-time' : 'time', - tz = time.substr(prefix.length + 1) || 'UTC'; + var time = $('#time').val() + var prefix = time.startsWith('natural-time') ? 'natural-time' : 'time' + var tz = time.substr(prefix.length + 1) || moment.tz.guess(); // Use browser's local TZ if not set + + var m = moment.unix(timestamp).tz(tz); + var fullTime = m.format('YYYY-MM-DD HH:mm:ss.SSS'); // full date/time without TZ if (prefix === 'natural-time') { - return moment.unix(timestamp).tz(tz).fromNow(); + return '' + m.fromNow() + ''; } - return moment.unix(timestamp).tz(tz).format('YYYY-MM-DD HH:mm:ss.SSS'); + + return '' + fullTime + ''; } function usesNaturalTime() { @@ -929,8 +933,7 @@ var flower = (function () { return format_time(data); } return ''; + '">' + format_time(data) + ''; } return data; } diff --git a/flower/views/tasks.py b/flower/views/tasks.py index 6d85e802..1fb28c24 100644 --- a/flower/views/tasks.py +++ b/flower/views/tasks.py @@ -1,7 +1,10 @@ import copy import logging +from zoneinfo import ZoneInfo from tornado import web +from tzlocal import get_localzone +from celery.utils.time import LocalTimezone from ..utils.search import QuerySyntaxError from ..utils.tasks import as_dict, get_task_by_id, search_tasks @@ -96,8 +99,25 @@ def get(self): capp = self.application.capp time = 'natural-time' if app.options.natural_time else 'time' - if capp.conf.timezone: - time += '-' + str(capp.conf.timezone) + + if not app.options.browser_local_time: + # Append Celery app timezone in IANA format + + timezone = None + + if capp.timezone: + if isinstance(capp.timezone, LocalTimezone): + try: + timezone = get_localzone() + except Exception as ex: + logger.warning("Failed to retrieve local timezone (%s): %s", type(ex).__name__, ex) + else: + timezone = capp.timezone + + if timezone is None: + timezone = ZoneInfo("UTC") + + time = f'{time}-{timezone}' self.render( "tasks.html", diff --git a/requirements/default.txt b/requirements/default.txt index 553aa6f3..405003c4 100644 --- a/requirements/default.txt +++ b/requirements/default.txt @@ -3,3 +3,4 @@ tornado>=6.5.7,<7.0.0 prometheus_client>=0.8.0 humanize pytz +tzlocal diff --git a/tests/unit/views/test_tasks.py b/tests/unit/views/test_tasks.py index 03e4d4af..d677d4f3 100644 --- a/tests/unit/views/test_tasks.py +++ b/tests/unit/views/test_tasks.py @@ -2,6 +2,7 @@ import time from celery.events import Event +from tzlocal import get_localzone from flower.events import EventsState from tests.unit import AsyncHTTPTestCase @@ -343,3 +344,66 @@ def test_pagination(self): self.assertEqual('task2', tasks[0]['name']) self.assertEqual('456', tasks[0]['uuid']) self.assertEqual('worker1', tasks[0]['worker']) + + +class TasksTimeZoneTest(AsyncHTTPTestCase): + + def test_config_time_zone_is_utc(self): + del self._app.capp.timezone # clear cached property + self._app.capp.conf.timezone = 'UTC' + self._app.capp.conf.enable_utc = False # should be ignored + self._app.options.browser_local_time = False + + r = self.get('/tasks') + self.assertEqual(200, r.code) + body = r.body.decode() + self.assertIn('time-UTC', body) + + def test_config_time_zone_is_celery_local(self): + del self._app.capp.timezone # clear cached property + self._app.capp.conf.timezone = 'Pacific/Chatham' + self._app.capp.conf.enable_utc = True # should be ignored + self._app.options.browser_local_time = False + + r = self.get('/tasks') + self.assertEqual(200, r.code) + body = r.body.decode() + + self.assertIn('time-Pacific/Chatham', body) + + def test_default_time_zone_is_utc(self): + del self._app.capp.timezone # clear cached property + self._app.capp.conf.enable_utc = True + self._app.options.browser_local_time = False + + r = self.get('/tasks') + self.assertEqual(200, r.code) + body = r.body.decode() + self.assertIn('time-UTC', body) + + def test_default_time_zone_is_system_local(self): + del self._app.capp.timezone # clear cached property + self._app.capp.conf.enable_utc = False + self._app.options.browser_local_time = False + + try: + expected_tz = get_localzone() + except Exception: + expected_tz = None + if expected_tz is None: + expected_tz = 'UTC' + + r = self.get('/tasks') + self.assertEqual(200, r.code) + body = r.body.decode() + self.assertIn(f'time-{expected_tz}', body) + + def test_browser_local_time(self): + del self._app.capp.timezone # clear cached property + self._app.capp.conf.timezone = 'Pacific/Chatham' + self._app.options.browser_local_time = True + + r = self.get('/tasks') + self.assertEqual(200, r.code) + body = r.body.decode() + self.assertIn('value="time"', body)