Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v1.10.5 #5151

Merged
merged 4 commits into from
Oct 9, 2024
Merged

v1.10.5 #5151

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
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
3 changes: 3 additions & 0 deletions engine/apps/alerts/models/alert.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length

if typing.TYPE_CHECKING:
from django.db.models.manager import RelatedManager

from apps.alerts.models import AlertGroup, AlertReceiveChannel, ChannelFilter

logger = logging.getLogger(__name__)
Expand All @@ -47,6 +49,7 @@ def generate_public_primary_key_for_alert():

class Alert(models.Model):
group: typing.Optional["AlertGroup"]
resolved_alert_groups: "RelatedManager['AlertGroup']"

public_primary_key = models.CharField(
max_length=20,
Expand Down
24 changes: 13 additions & 11 deletions engine/apps/alerts/models/alert_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from django.db.models import JSONField, Q, QuerySet
from django.utils import timezone
from django.utils.functional import cached_property
from django_deprecate_fields import deprecate_field

from apps.alerts.constants import ActionSource, AlertGroupState
from apps.alerts.escalation_snapshot import EscalationSnapshotMixin
Expand Down Expand Up @@ -288,17 +287,20 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
related_name="resolved_alert_groups",
)

# NOTE: see https://raintank-corp.slack.com/archives/C07RGREUH4Z/p1728494111646319
# This field should eventually be dropped as it's no longer being set/read anywhere
resolved_by_alert = deprecate_field(
models.ForeignKey(
"alerts.Alert",
on_delete=models.SET_NULL,
null=True,
default=None,
related_name="resolved_alert_groups",
)
resolved_by_alert = models.ForeignKey(
"alerts.Alert",
on_delete=models.SET_NULL,
null=True,
default=None,
related_name="resolved_alert_groups",
)
"""
⚠️ This field is no longer being set/read anywhere, DON'T USE IT! ⚠️

TODO: We still need to figure out how to remove it safely.

See [this conversation](https://raintank-corp.slack.com/archives/C07RGREUH4Z/p1728494111646319) for more context
"""

resolved_at = models.DateTimeField(blank=True, null=True)
acknowledged = models.BooleanField(default=False)
Expand Down
14 changes: 14 additions & 0 deletions engine/apps/alerts/tasks/notify_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,20 @@ def perform_notification(log_record_pk, use_default_notification_policy_fallback
).save()
return

if alert_group.resolved:
# skip notification if alert group was resolved
UserNotificationPolicyLogRecord(
author=user,
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED,
notification_policy=notification_policy,
reason="Skipped notification because alert group is resolved",
alert_group=alert_group,
notification_step=notification_policy.step if notification_policy else None,
notification_channel=notification_channel,
notification_error_code=None,
).save()
return

if notification_channel == UserNotificationPolicy.NotificationChannel.SMS:
phone_backend = PhoneBackend()
phone_backend.notify_by_sms(user, alert_group, notification_policy)
Expand Down
32 changes: 32 additions & 0 deletions engine/apps/alerts/tests/test_notify_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,38 @@ def test_notify_user_error_if_viewer(
assert error_log_record.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_FORBIDDEN


@pytest.mark.django_db
def test_notify_user_perform_notification_skip_if_resolved(
make_organization,
make_user,
make_user_notification_policy,
make_alert_receive_channel,
make_alert_group,
make_user_notification_policy_log_record,
):
organization = make_organization()
user_1 = make_user(organization=organization, _verified_phone_number="1234567890")
user_notification_policy = make_user_notification_policy(
user=user_1,
step=UserNotificationPolicy.Step.NOTIFY,
notify_by=UserNotificationPolicy.NotificationChannel.SMS,
)
alert_receive_channel = make_alert_receive_channel(organization=organization)
alert_group = make_alert_group(alert_receive_channel=alert_receive_channel, resolved=True)
log_record = make_user_notification_policy_log_record(
author=user_1,
alert_group=alert_group,
notification_policy=user_notification_policy,
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_TRIGGERED,
)

perform_notification(log_record.pk, False)

error_log_record = UserNotificationPolicyLogRecord.objects.last()
assert error_log_record.type == UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED
assert error_log_record.reason == "Skipped notification because alert group is resolved"


@pytest.mark.django_db
@pytest.mark.parametrize(
"reason_to_skip_escalation,error_code",
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
12 changes: 4 additions & 8 deletions engine/requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,21 @@ charset-normalizer==3.3.2
# requests
distlib==0.3.8
# via virtualenv
django==4.2.15
django==4.2.16
# via
# -c requirements.txt
# django-stubs
# django-stubs-ext
django-filter-stubs==0.1.3
# via -r requirements-dev.in
django-stubs==4.2.2
django-stubs[compatible-mypy]==4.2.2
# via
# -r requirements-dev.in
# django-filter-stubs
# djangorestframework-stubs
django-stubs-ext==4.2.7
# via django-stubs
djangorestframework-stubs==3.14.2
djangorestframework-stubs[compatible-mypy]==3.14.2
# via
# -r requirements-dev.in
# django-filter-stubs
Expand Down Expand Up @@ -96,7 +96,7 @@ pytest-django==4.8.0
# via -r requirements-dev.in
pytest-factoryboy==2.7.0
# via -r requirements-dev.in
pytest-xdist==3.6.1
pytest-xdist[psutil]==3.6.1
# via -r requirements-dev.in
python-dateutil==2.8.2
# via
Expand All @@ -110,10 +110,6 @@ requests==2.32.3
# via
# -c requirements.txt
# djangorestframework-stubs
setuptools==73.0.0
# via
# -c requirements.txt
# nodeenv
six==1.16.0
# via
# -c requirements.txt
Expand Down
2 changes: 1 addition & 1 deletion engine/requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ babel==2.12.1
beautifulsoup4==4.12.2
celery[redis]==5.3.1
cryptography==43.0.1
django==4.2.15
django==4.2.16
django-add-default-value==0.10.0
django-amazon-ses==4.0.1
django-anymail==11.1
Expand Down
Loading
Loading