Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/man.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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*)
Expand Down
2 changes: 2 additions & 0 deletions flower/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
17 changes: 10 additions & 7 deletions flower/static/js/flower.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<span title="' + fullTime + ' (' + tz + ')">' + m.fromNow() + '</span>';
}
return moment.unix(timestamp).tz(tz).format('YYYY-MM-DD HH:mm:ss.SSS');

return '<span title="' + m.fromNow() + ' (' + tz + ')">' + fullTime + '</span>';
}

function usesNaturalTime() {
Expand Down Expand Up @@ -929,8 +933,7 @@ var flower = (function () {
return format_time(data);
}
return '<time datetime="' + moment.unix(data).toISOString() +
'" title="' + moment.unix(data).fromNow() + '">' +
format_time(data) + '</time>';
'">' + format_time(data) + '</time>';
}
return data;
}
Expand Down
24 changes: 22 additions & 2 deletions flower/views/tasks.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions requirements/default.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ tornado>=6.5.7,<7.0.0
prometheus_client>=0.8.0
humanize
pytz
tzlocal
64 changes: 64 additions & 0 deletions tests/unit/views/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Loading