From a0dae78f5c257e8f4a12a9fab7616ba69a16fb6a Mon Sep 17 00:00:00 2001 From: woutdenolf Date: Mon, 22 Jan 2024 19:20:13 +0100 Subject: [PATCH 1/4] support enable_utc in addition to timezone: let celery handle the timezone to support future changes --- docs/config.rst | 10 ++++++ docs/man.rst | 1 + flower/options.py | 2 ++ flower/static/js/flower.js | 17 +++++----- flower/views/tasks.py | 17 ++++++++-- requirements/default.txt | 1 + tests/unit/views/test_tasks.py | 57 ++++++++++++++++++++++++++++++++++ 7 files changed, 96 insertions(+), 9 deletions(-) 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..47ae3205 100644 --- a/flower/views/tasks.py +++ b/flower/views/tasks.py @@ -1,7 +1,10 @@ import copy import logging +import pytz 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,18 @@ 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 + if capp.timezone: + if isinstance(capp.timezone, LocalTimezone): + timezone = get_localzone() + else: + timezone = capp.timezone + else: + timezone = pytz.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..66a07df8 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,59 @@ 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(f'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 + + r = self.get('/tasks') + self.assertEqual(200, r.code) + body = r.body.decode() + self.assertIn(f'time-{get_localzone()}', 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(f'time', body) From b3dce4de964da5d93182c0b400fcad4d0906df64 Mon Sep 17 00:00:00 2001 From: woutdenolf Date: Wed, 13 Aug 2025 19:17:31 +0200 Subject: [PATCH 2/4] fix after review --- flower/views/tasks.py | 4 ++-- tests/unit/views/test_tasks.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flower/views/tasks.py b/flower/views/tasks.py index 47ae3205..1029699a 100644 --- a/flower/views/tasks.py +++ b/flower/views/tasks.py @@ -1,7 +1,7 @@ import copy import logging +from zoneinfo import ZoneInfo -import pytz from tornado import web from tzlocal import get_localzone from celery.utils.time import LocalTimezone @@ -108,7 +108,7 @@ def get(self): else: timezone = capp.timezone else: - timezone = pytz.utc + timezone = ZoneInfo("UTC") time = f'{time}-{timezone}' diff --git a/tests/unit/views/test_tasks.py b/tests/unit/views/test_tasks.py index 66a07df8..78061639 100644 --- a/tests/unit/views/test_tasks.py +++ b/tests/unit/views/test_tasks.py @@ -369,7 +369,7 @@ def test_config_time_zone_is_celery_local(self): self.assertEqual(200, r.code) body = r.body.decode() - self.assertIn(f'time-Pacific/Chatham', body) + self.assertIn('time-Pacific/Chatham', body) def test_default_time_zone_is_utc(self): del self._app.capp.timezone # clear cached property @@ -399,4 +399,4 @@ def test_browser_local_time(self): r = self.get('/tasks') self.assertEqual(200, r.code) body = r.body.decode() - self.assertIn(f'time', body) + self.assertIn('time', body) From 7f5ff5c4db6ac2f2fab0ce25994f67f8ec11ade8 Mon Sep 17 00:00:00 2001 From: woutdenolf Date: Wed, 13 Aug 2025 21:36:21 +0200 Subject: [PATCH 3/4] get_localzone could return None or raise an exception in rare cases --- flower/views/tasks.py | 11 +++++++++-- tests/unit/views/test_tasks.py | 9 ++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/flower/views/tasks.py b/flower/views/tasks.py index 1029699a..1fb28c24 100644 --- a/flower/views/tasks.py +++ b/flower/views/tasks.py @@ -102,12 +102,19 @@ def get(self): if not app.options.browser_local_time: # Append Celery app timezone in IANA format + + timezone = None + if capp.timezone: if isinstance(capp.timezone, LocalTimezone): - timezone = get_localzone() + 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 - else: + + if timezone is None: timezone = ZoneInfo("UTC") time = f'{time}-{timezone}' diff --git a/tests/unit/views/test_tasks.py b/tests/unit/views/test_tasks.py index 78061639..1e961e70 100644 --- a/tests/unit/views/test_tasks.py +++ b/tests/unit/views/test_tasks.py @@ -386,10 +386,17 @@ def test_default_time_zone_is_system_local(self): 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-{get_localzone()}', body) + self.assertIn(f'time-{expected_tz}', body) def test_browser_local_time(self): del self._app.capp.timezone # clear cached property From bde7bd027ef74ea2c975cef19d8ed3b7b8646115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asif=20Saif=20Uddin=20=7B=22Auvi=22=3A=22=E0=A6=85?= =?UTF-8?q?=E0=A6=AD=E0=A6=BF=22=7D?= Date: Thu, 3 Sep 2026 23:22:21 +0600 Subject: [PATCH 4/4] Update assertion to check for 'value="time"' in response Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/unit/views/test_tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/views/test_tasks.py b/tests/unit/views/test_tasks.py index 1e961e70..d677d4f3 100644 --- a/tests/unit/views/test_tasks.py +++ b/tests/unit/views/test_tasks.py @@ -406,4 +406,4 @@ def test_browser_local_time(self): r = self.get('/tasks') self.assertEqual(200, r.code) body = r.body.decode() - self.assertIn('time', body) + self.assertIn('value="time"', body)