From 4df898528351bc70079785501522b78ad0fabb46 Mon Sep 17 00:00:00 2001 From: jorgeav <54142549+jorgeav@users.noreply.github.com> Date: Tue, 5 Dec 2023 05:39:04 +1100 Subject: [PATCH] Jinja2 template helper filter datetimeformat_as_timezone (#3426) # What this PR does Add an additional jinja2 template helper filter to convert a timezone aware datetime to a different timezone. ## Which issue(s) this PR fixes Alert payloads that originate from different time zones may include timestamps having a local time offset. This filter enables standardization of timestamp timezones. ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --------- Co-authored-by: Joey Orlando --- CHANGELOG.md | 4 ++ docs/sources/jinja2-templating/_index.md | 8 ++- engine/common/jinja_templater/filters.py | 8 +++ .../jinja_templater/jinja_template_env.py | 2 + .../common/tests/test_apply_jinja_template.py | 57 +++++++++++++++++++ engine/common/tests/test_base64decode.py | 7 --- .../CheatSheet/CheatSheet.config.ts | 2 +- 7 files changed, 78 insertions(+), 10 deletions(-) delete mode 100644 engine/common/tests/test_base64decode.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b768aaa7..1efb5afb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Add `datetimeformat_as_timezone` Jinja2 template helper filter by @jorgeav ([#3426](https://github.com/grafana/oncall/pull/3426)) + ### Changed - Disallow creating and deleting direct paging integrations by @vadimkerr ([#3475](https://github.com/grafana/oncall/pull/3475)) diff --git a/docs/sources/jinja2-templating/_index.md b/docs/sources/jinja2-templating/_index.md index b3e1d311..64d3489c 100644 --- a/docs/sources/jinja2-templating/_index.md +++ b/docs/sources/jinja2-templating/_index.md @@ -205,8 +205,12 @@ Built-in functions: - `tojson_pretty` - same as tojson, but prettified - `iso8601_to_time` - converts time from iso8601 (`2015-02-17T18:30:20.000Z`) to datetime - `datetimeformat` - converts time from datetime to the given format (`%H:%M / %d-%m-%Y` by default) +- `datetimeformat_as_timezone` - same as `datetimeformat`, with the inclusion of timezone conversion (`UTC` by default) + - Usage example: `{{ payload.alerts.startsAt | iso8601_to_time | datetimeformat_as_timezone('%Y-%m-%dT%H:%M:%S%z', 'America/Chicago') }}` - `regex_replace` - performs a regex find and replace -- `regex_match` - performs a regex match, returns `True` or `False`. Usage example: `{{ payload.ruleName | regex_match(".*") }}` -- `b64decode` - performs a base64 string decode. Usage example: `{{ payload.data | b64decode }}` +- `regex_match` - performs a regex match, returns `True` or `False` + - Usage example: `{{ payload.ruleName | regex_match(".*") }}` +- `b64decode` - performs a base64 string decode + - Usage example: `{{ payload.data | b64decode }}` {{< section >}} diff --git a/engine/common/jinja_templater/filters.py b/engine/common/jinja_templater/filters.py index e24d01ee..a14a3ad5 100644 --- a/engine/common/jinja_templater/filters.py +++ b/engine/common/jinja_templater/filters.py @@ -3,6 +3,7 @@ import json import re from django.utils.dateparse import parse_datetime +from pytz import timezone def datetimeformat(value, format="%H:%M / %d-%m-%Y"): @@ -12,6 +13,13 @@ def datetimeformat(value, format="%H:%M / %d-%m-%Y"): return None +def datetimeformat_as_timezone(value, format="%H:%M / %d-%m-%Y", tz="UTC"): + try: + return value.astimezone(timezone(tz)).strftime(format) + except (ValueError, AttributeError, TypeError): + return None + + def iso8601_to_time(value): try: return parse_datetime(value) diff --git a/engine/common/jinja_templater/jinja_template_env.py b/engine/common/jinja_templater/jinja_template_env.py index 21c227c7..1941bff8 100644 --- a/engine/common/jinja_templater/jinja_template_env.py +++ b/engine/common/jinja_templater/jinja_template_env.py @@ -6,6 +6,7 @@ from jinja2.sandbox import SandboxedEnvironment from .filters import ( b64decode, datetimeformat, + datetimeformat_as_timezone, iso8601_to_time, json_dumps, regex_match, @@ -22,6 +23,7 @@ def raise_security_exception(name): jinja_template_env = SandboxedEnvironment(loader=BaseLoader()) jinja_template_env.filters["datetimeformat"] = datetimeformat +jinja_template_env.filters["datetimeformat_as_timezone"] = datetimeformat_as_timezone jinja_template_env.filters["iso8601_to_time"] = iso8601_to_time jinja_template_env.filters["tojson_pretty"] = to_pretty_json jinja_template_env.globals["time"] = timezone.now diff --git a/engine/common/tests/test_apply_jinja_template.py b/engine/common/tests/test_apply_jinja_template.py index aab3f488..e2c0dc3b 100644 --- a/engine/common/tests/test_apply_jinja_template.py +++ b/engine/common/tests/test_apply_jinja_template.py @@ -1,7 +1,10 @@ +import base64 import json import pytest from django.conf import settings +from django.utils.dateparse import parse_datetime +from pytz import timezone from common.jinja_templater import apply_jinja_template from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning @@ -14,6 +17,60 @@ def test_apply_jinja_template(): assert payload == result +def test_apply_jinja_template_iso8601_to_time(): + payload = {"name": "2023-11-22T15:30:00.000000000Z"} + + result = apply_jinja_template( + "{{ payload.name | iso8601_to_time }}", + payload, + ) + expected = str(parse_datetime(payload["name"])) + assert result == expected + + +def test_apply_jinja_template_datetimeformat(): + payload = {"aware": "2023-05-28 23:11:12+0000", "naive": "2023-05-28 23:11:12"} + + assert apply_jinja_template( + "{{ payload.aware | iso8601_to_time | datetimeformat('%Y-%m-%dT%H:%M:%S%z') }}", + payload, + ) == parse_datetime(payload["aware"]).strftime("%Y-%m-%dT%H:%M:%S%z") + assert apply_jinja_template( + "{{ payload.naive | iso8601_to_time | datetimeformat('%Y-%m-%dT%H:%M:%S%z') }}", + payload, + ) == parse_datetime(payload["naive"]).strftime("%Y-%m-%dT%H:%M:%S%z") + + +def test_apply_jinja_template_datetimeformat_as_timezone(): + payload = {"aware": "2023-05-28 23:11:12+0000", "naive": "2023-05-28 23:11:12"} + + assert apply_jinja_template( + "{{ payload.aware | iso8601_to_time | datetimeformat_as_timezone('%Y-%m-%dT%H:%M:%S%z', 'America/Chicago') }}", + payload, + ) == parse_datetime(payload["aware"]).astimezone(timezone("America/Chicago")).strftime("%Y-%m-%dT%H:%M:%S%z") + assert apply_jinja_template( + "{{ payload.naive | iso8601_to_time | datetimeformat_as_timezone('%Y-%m-%dT%H:%M:%S%z', 'America/Chicago') }}", + payload, + ) == parse_datetime(payload["naive"]).astimezone(timezone("America/Chicago")).strftime("%Y-%m-%dT%H:%M:%S%z") + + with pytest.raises(JinjaTemplateWarning): + apply_jinja_template( + "{{ payload.aware | iso8601_to_time | datetimeformat_as_timezone('%Y-%m-%dT%H:%M:%S%z', 'potato') }}", + payload, + ) + + +def test_apply_jinja_template_b64decode(): + payload = {"name": "SGVsbG8sIHdvcmxkIQ=="} + + assert apply_jinja_template( + "{{ payload.name | b64decode }}", + payload, + ) == base64.b64decode( + payload["name"] + ).decode("utf-8") + + def test_apply_jinja_template_json_dumps(): payload = {"name": "test"} diff --git a/engine/common/tests/test_base64decode.py b/engine/common/tests/test_base64decode.py deleted file mode 100644 index 59fbf666..00000000 --- a/engine/common/tests/test_base64decode.py +++ /dev/null @@ -1,7 +0,0 @@ -from common.jinja_templater.filters import b64decode - - -def test_base64_decode(): - original = "dGVzdCBzdHJpbmch" - expected = "test string!" - assert b64decode(original) == expected diff --git a/grafana-plugin/src/components/CheatSheet/CheatSheet.config.ts b/grafana-plugin/src/components/CheatSheet/CheatSheet.config.ts index 506522ae..478f70aa 100644 --- a/grafana-plugin/src/components/CheatSheet/CheatSheet.config.ts +++ b/grafana-plugin/src/components/CheatSheet/CheatSheet.config.ts @@ -80,7 +80,7 @@ export const genericTemplateCheatSheet: CheatSheetInterface = { { listItemName: 'payload - payload of last alert in the group' }, { listItemName: 'web_title, web_mesage, web_image_url - templates from Web' }, { listItemName: 'payload, grafana_oncall_link, grafana_oncall_incident_id, integration_name, source_link' }, - { listItemName: 'time(), datetimeformat, iso8601_to_time' }, + { listItemName: 'time(), datetimeformat, datetimeformat_as_timezone, iso8601_to_time' }, { listItemName: 'to_pretty_json' }, { listItemName: 'regex_replace, regex_match' }, { listItemName: 'b64decode' },