Andy McKay

May 26, 2023

Service catalog - Timezone support


When you are in an incident that’s causing impact to your customers, you’ll be looking at data in multiple sources. From events on GitHub, messages on Slack, logs in Splunk, graphs in Datadog and on and on. At the time of an incident there’s a lot going on and one thing that always caused a bit more cognitive stress for me was calculating when everything happened. To add to this, you might be working with people who are in diverse time zones. Most services (thank goodness) these days log in UTC and the service catalog is no exception, logging things in UTC.

To make life just that bit easier, the service catalog now allows you to set a timezone and whenever you see a date, you’ll see it 3 different ways:

  • The time relative to now
  • The time in UTC
  • The time in your timezone

Here’s an example:

This may seem like a small thing, but this, along with the little copy to clipboard make it easier to communicate and understand what happened. Since the service catalog can capture important arbitrary events, such as deployments, migrations, releases, feature flag changes in catalog events, it’s easy to establish a timeline of what happened.

It’s been a while since I’ve done Django and this was a good learning experience through the timezone support which has been added over the years, and as usual was done really well. There’s a few things I’m going to highlight that I thought were good, but this document covers all the basics.

Settings

You should have TIME_ZONE set to UTC and USE_TZ set to True. I’ve worked on projects where the timestamp in the database is not in UTC and it just made me cry. Just leave this as UTC and never change it. There is only one timezone that data should ever be stored and it should always be UTC. Just don’t change that.

Letting users pick a timezone

You’ll probably have to make a user profile you ask the user for their timezone and then attach it to the user. The service catalog is tightly coupled to GitHub. In GitHub you also get to specify your timezone, but the REST API doesn’t include this information 1.

If you are looking for a list of all the timezones, then the zoneinfo</apy> package has you covered, no need to use pytz anymore.

With a model like this:

zones = sorted(zoneinfo.available_timezones())


class Profile(models.Model):
    TIMEZONE_CHOICES = zip(zones, zones)
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    timezone = models.CharField(max_length=255, choices=TIMEZONE_CHOICES, default="UTC")

You’ll quickly have a nice form for the users.

Using that timezone

Again, as noted in that document, you’ll want to activate the timezone for that user as they hit the site.

class TimezoneMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        tzname = request.session.get("tz")
        if not tzname:
            try:
                tzname = request.user.profile.timezone
            except (AttributeError):
                pass

        # You've got settings.TIME_ZONE set to "UTC" right? So that becomes the default.
        tzname = tzname or settings.TIME_ZONE
        timezone.activate(zoneinfo.ZoneInfo(tzname))

        # Stuff into the session for the next request.
        request.session["tz"] = tzname
        return self.get_response(request)

This is a pretty simplified middleware that activates the timezone and stuffs it in the session. So what’s this going to do?

The date and time will be stored in the database as UTC as nature intended. But whenever it’s shown to a user, it will be translated into their timezone. When you have a form, users enter the date and time in their timezone and Django stores it as UTC. This is pretty much exactly what you want in a web site.

Gotchas

To show the date in UTC in a template, I used the utc filter. You can also use the {% timezone None %} (as documented) or localtime. Otherwise Django was being smart and trying to convert into the timezone for me.

A model has default=datetime.now on it, meaning a HTML form defaulted to showing the time in UTC. This was simple to fix in the view, because when the view was being called, the middleware (see above) had been called, allowing us to know the users timezone. So then in the view, I simply switched to the users timezone:

        form = EventForm()
        form.fields["start"].initial = timezone.now()

All this is well and good for the web interface, but what about the API? API’s should always be in UTC and again that’s something non-negotiable in my opinion. In the middleware, if the request is coming into an API endpoint (it’s using Django Rest Framework) then the timezone just remains as UTC.

Conclusion

That’s it, nothing to revolutionary in this post, but just a pile of stuff that’s builtin to Django and just gets it right in my opinion.

  1. I couldn’t spot it in GraphQL either, sometimes the two aren’t in sync so it might be there, couldn’t find it though.