Skip to content

Commit

Permalink
feat: add timedeltaparse function to the jinja template (#5142)
Browse files Browse the repository at this point in the history
# What this PR does
If an OnCall template needs to contain a date relative to a date in the
alert response, currently there is no way for the template to add or
subtract time from a parsed date.

This PR adds a function that allows a time-window (e.g., 1s, 5m, 6h, 7d,
2w) to be converted into a Python timedelta, which can then be added or
subtracted from a datetime. An example usage might be:

```
{% set delta = alert.timeWindow | timedeltaparse %}
{% set time = alert.startsAt | iso8601_to_time - delta | datetimeformat('%s') %}
```

## 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] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will show up in the
autogenerated release notes.
  • Loading branch information
kelnage authored Oct 9, 2024
1 parent b79c5d0 commit cf589d4
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ Grafana OnCall enhances Jinja with additional functions:
- `datetimeformat_as_timezone`: Converts datetime to string with 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') }}`
- `datetimeparse`: Converts string to datetime according to strftime format codes (`%H:%M / %d-%m-%Y` by default)
- `timedeltaparse`: Converts a time range (e.g., `5s`, `2m`, `6h`, `3d`) to a timedelta that can be added to or subtracted from a datetime
- Usage example: `{% set delta = alert.window | timedeltaparse %}{{ alert.startsAt | iso8601_to_time - delta | datetimeformat }}`
- `regex_replace`: Performs a regex find and replace
- `regex_match`: Performs a regex match, returns `True` or `False`
- Usage example: `{{ payload.ruleName | regex_match(".*") }}`
Expand Down
31 changes: 30 additions & 1 deletion engine/common/jinja_templater/filters.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import base64
import json
from datetime import datetime
from datetime import datetime, timedelta

import regex
from django.utils.dateparse import parse_datetime
Expand Down Expand Up @@ -37,6 +37,35 @@ def iso8601_to_time(value):
return None


range_duration_re = regex.compile("^(?P<sign>[-+]?)(?P<amount>\\d+)(?P<unit>[smhdwMQy])$")


def timedeltaparse(value):
try:
match = range_duration_re.match(value)
if match:
kw = match.groupdict()
amount = int(kw["amount"])
if kw["sign"] == "-":
amount = -amount
if kw["unit"] == "s":
return timedelta(seconds=amount)
elif kw["unit"] == "m":
return timedelta(minutes=amount)
elif kw["unit"] == "h":
return timedelta(hours=amount)
elif kw["unit"] == "d":
return timedelta(days=amount)
elif kw["unit"] == "w":
return timedelta(weeks=amount)
# The remaining units (MQy) are not supported by timedelta
else:
return None
except (ValueError, AttributeError, TypeError):
return None
return None


def to_pretty_json(value):
try:
return json.dumps(value, sort_keys=True, indent=4, separators=(",", ": "), ensure_ascii=False)
Expand Down
2 changes: 2 additions & 0 deletions engine/common/jinja_templater/jinja_template_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
regex_match,
regex_replace,
regex_search,
timedeltaparse,
to_pretty_json,
)

Expand All @@ -28,6 +29,7 @@ def raise_security_exception(name):
jinja_template_env.filters["datetimeformat_as_timezone"] = datetimeformat_as_timezone
jinja_template_env.filters["datetimeparse"] = datetimeparse
jinja_template_env.filters["iso8601_to_time"] = iso8601_to_time
jinja_template_env.filters["timedeltaparse"] = timedeltaparse
jinja_template_env.filters["tojson_pretty"] = to_pretty_json
jinja_template_env.globals["time"] = timezone.now
jinja_template_env.globals["range"] = lambda *args: raise_security_exception("range")
Expand Down
43 changes: 42 additions & 1 deletion engine/common/tests/test_apply_jinja_template.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import base64
import json
from datetime import datetime
from datetime import datetime, timedelta
from unittest.mock import patch

import pytest
Expand Down Expand Up @@ -140,6 +140,47 @@ def test_apply_jinja_template_datetimeparse():
) == str(datetime.strptime(payload["naive"], "%Y-%m-%dT%H:%M:%S"))


def test_apply_jinja_template_timedeltaparse():
payload = {"seconds": "-100s", "hours": "12h", "days": "-5d", "weeks": "52w"}

assert apply_jinja_template(
"{{ payload.seconds | timedeltaparse }}",
payload,
) == str(timedelta(seconds=-100))
assert apply_jinja_template(
"{{ payload.hours | timedeltaparse }}",
payload,
) == str(timedelta(hours=12))
assert apply_jinja_template(
"{{ payload.days | timedeltaparse }}",
payload,
) == str(timedelta(days=-5))
assert apply_jinja_template(
"{{ payload.weeks | timedeltaparse }}",
payload,
) == str(timedelta(weeks=52))


def test_apply_jinja_template_timedelta_arithmetic():
payload = {
"dt": "2023-11-22T15:30:00.000000000Z",
"delta": "1h",
"before": "2023-11-22T14:30:00.000000000Z",
"after": "2023-11-22T16:30:00.000000000Z",
}

result = apply_jinja_template(
"{% set delta = payload.delta | timedeltaparse -%}{{ payload.dt | iso8601_to_time - delta }}",
payload,
)
assert result == str(parse_datetime(payload["before"]))
result = apply_jinja_template(
"{% set delta = payload.delta | timedeltaparse -%}{{ payload.dt | iso8601_to_time + delta }}",
payload,
)
assert result == str(parse_datetime(payload["after"]))


def test_apply_jinja_template_b64decode():
payload = {"name": "SGVsbG8sIHdvcmxkIQ=="}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const genericTemplateCheatSheet: CheatSheetInterface = {
{ listItemName: 'labels - labels assigned to the 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, datetimeformat_as_timezone, datetimeparse, iso8601_to_time' },
{ listItemName: 'time(), datetimeformat, datetimeformat_as_timezone, datetimeparse, iso8601_to_time, timedeltaparse' },
{ listItemName: 'to_pretty_json' },
{ listItemName: 'regex_replace, regex_match, regex_search' },
{ listItemName: 'b64decode' },
Expand Down

0 comments on commit cf589d4

Please sign in to comment.