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)