Skip to content

Commit

Permalink
Merge pull request #278 from grafana/dev
Browse files Browse the repository at this point in the history
Merge dev to main
  • Loading branch information
Konstantinov-Innokentii authored Jul 22, 2022
2 parents 7627853 + c9c6df8 commit 4520f9f
Show file tree
Hide file tree
Showing 17 changed files with 478 additions and 45 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Change Log

## v1.0.10 (2022-07-22)
- Speed-up of alert group web caching
- Internal api for OnCall shifts

## v1.0.9 (2022-07-21)
- Frontend bug fixes & improvements
- Support regex_replace() in templates
Expand Down
13 changes: 9 additions & 4 deletions engine/apps/alerts/models/alert_receive_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -694,14 +694,19 @@ def listen_for_alertreceivechannel_model_save(sender, instance, created, *args,
instance.organization, None, OrganizationLogType.TYPE_HEARTBEAT_CREATED, description
)
else:
logger.info(f"Drop AG cache. Reason: save alert_receive_channel {instance.pk}")
if kwargs is not None:
if "update_fields" in kwargs:
if kwargs["update_fields"] is not None:
fields_to_not_to_invalidate_cache = [
"rate_limit_message_task_id",
"rate_limited_in_slack_at",
"reason_to_skip_escalation",
]
# Hack to not to invalidate web cache on AlertReceiveChannel.start_send_rate_limit_message_task
if "rate_limit_message_task_id" in kwargs["update_fields"]:
return

for f in fields_to_not_to_invalidate_cache:
if f in kwargs["update_fields"]:
return
logger.info(f"Drop AG cache. Reason: save alert_receive_channel {instance.pk}")
invalidate_web_cache_for_alert_group.apply_async(kwargs={"channel_pk": instance.pk})

if instance.integration == AlertReceiveChannel.INTEGRATION_GRAFANA_ALERTING:
Expand Down
8 changes: 6 additions & 2 deletions engine/apps/alerts/tests/test_escalation_policy_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,10 @@ def test_escalation_step_notify_on_call_schedule(

schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar)
# create on_call_shift with user to notify
start_date = timezone.datetime.now().replace(microsecond=0)
data = {
"start": timezone.datetime.now().replace(microsecond=0),
"start": start_date,
"rotation_start": start_date,
"duration": timezone.timedelta(seconds=7200),
}
on_call_shift = make_on_call_shift(
Expand Down Expand Up @@ -216,8 +218,10 @@ def test_escalation_step_notify_on_call_schedule_viewer_user(

schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar)
# create on_call_shift with user to notify
start_date = timezone.datetime.now().replace(microsecond=0)
data = {
"start": timezone.datetime.now().replace(microsecond=0),
"start": start_date,
"rotation_start": start_date,
"duration": timezone.timedelta(seconds=7200),
}
on_call_shift = make_on_call_shift(
Expand Down
1 change: 1 addition & 0 deletions engine/apps/alerts/tests/test_terraform_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def test_render_terraform_file(
interval=1,
week_start=CustomOnCallShift.MONDAY,
start=dateparse.parse_datetime("2021-08-16T17:00:00"),
rotation_start=dateparse.parse_datetime("2021-08-16T17:00:00"),
duration=timezone.timedelta(seconds=3600),
by_day=["MO", "SA"],
rolling_users=[{user.pk: user.public_primary_key}],
Expand Down
203 changes: 203 additions & 0 deletions engine/apps/api/serializers/on_call_shifts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
from rest_framework import serializers

from apps.schedules.models import CustomOnCallShift, OnCallScheduleWeb
from apps.user_management.models import User
from common.api_helpers.custom_fields import (
OrganizationFilteredPrimaryKeyRelatedField,
RollingUsersField,
UsersFilteredByOrganizationField,
)
from common.api_helpers.mixins import EagerLoadingMixin
from common.api_helpers.utils import CurrentOrganizationDefault


class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer):
id = serializers.CharField(read_only=True, source="public_primary_key")
organization = serializers.HiddenField(default=CurrentOrganizationDefault())
type = serializers.ChoiceField(
required=True,
choices=CustomOnCallShift.WEB_TYPES,
)
schedule = OrganizationFilteredPrimaryKeyRelatedField(queryset=OnCallScheduleWeb.objects)
frequency = serializers.ChoiceField(required=False, choices=CustomOnCallShift.FREQUENCY_CHOICES, allow_null=True)
shift_start = serializers.DateTimeField(source="start")
shift_end = serializers.SerializerMethodField()
by_day = serializers.ListField(required=False, allow_null=True)
rolling_users = RollingUsersField(
allow_null=True,
required=False,
child=UsersFilteredByOrganizationField(
queryset=User.objects, required=False, allow_null=True
), # todo: filter by team?
)

class Meta:
model = CustomOnCallShift
fields = [
"id",
"organization",
"name",
"type",
"schedule",
"priority_level",
"shift_start",
"shift_end",
"rotation_start",
"until",
"frequency",
"interval",
"by_day",
"source",
"rolling_users",
]
extra_kwargs = {
"interval": {"required": False, "allow_null": True},
"source": {"required": False, "write_only": True},
}

SELECT_RELATED = ["schedule"]

def get_shift_end(self, obj):
return obj.start + obj.duration

def to_internal_value(self, data):
data["source"] = CustomOnCallShift.SOURCE_WEB
data["week_start"] = CustomOnCallShift.MONDAY
if not data.get("shift_end"):
raise serializers.ValidationError({"shift_end": ["This field is required."]})

result = super().to_internal_value(data)
return result

def to_representation(self, instance):
result = super().to_representation(instance)
return result

def validate_name(self, name):
organization = self.context["request"].auth.organization
if name is None:
return name
try:
obj = CustomOnCallShift.objects.get(organization=organization, name=name)
except CustomOnCallShift.DoesNotExist:
return name
if self.instance and obj.id == self.instance.id:
return name
else:
raise serializers.ValidationError(["On-call shift with this name already exists"])

def validate_by_day(self, by_day):
if by_day:
for day in by_day:
if day not in CustomOnCallShift.WEB_WEEKDAY_MAP:
raise serializers.ValidationError(["Invalid day value."])
return by_day

def validate_interval(self, interval):
if interval is not None:
if not isinstance(interval, int) or interval <= 0:
raise serializers.ValidationError(["Invalid value"])
return interval

def validate_rolling_users(self, rolling_users):
result = []
if rolling_users:
for users in rolling_users:
users_dict = dict()
for user in users:
users_dict[user.pk] = user.public_primary_key
result.append(users_dict)
return result

def _validate_shift_end(self, start, end):
if end <= start:
raise serializers.ValidationError({"shift_end": ["Incorrect shift end date"]})

def _validate_frequency(self, frequency, event_type, rolling_users, interval, by_day):
if frequency is None:
if rolling_users and len(rolling_users) > 1:
raise serializers.ValidationError(
{"rolling_users": ["Cannot set multiple user groups for non-recurrent shifts"]}
)
if interval is not None:
raise serializers.ValidationError({"interval": ["Cannot set interval for non-recurrent shifts"]})
if by_day:
raise serializers.ValidationError({"by_day": ["Cannot set days value for non-recurrent shifts"]})
else:
if event_type == CustomOnCallShift.TYPE_OVERRIDE:
raise serializers.ValidationError(
{"frequency": ["Cannot set 'frequency' for shifts with type 'override'"]}
)
if frequency != CustomOnCallShift.FREQUENCY_WEEKLY and by_day:
raise serializers.ValidationError({"by_day": ["Cannot set days value for this frequency type"]})

def _validate_rotation_start(self, shift_start, rotation_start):
if rotation_start < shift_start:
raise serializers.ValidationError({"rotation_start": ["Incorrect rotation start date"]})

def _validate_until(self, rotation_start, until):
if until is not None and until < rotation_start:
raise serializers.ValidationError({"until": ["Incorrect rotation end date"]})

def _correct_validated_data(self, event_type, validated_data):
fields_to_update_for_overrides = [
"priority_level",
"frequency",
"interval",
"by_day",
"until",
"rotation_start",
]
if event_type == CustomOnCallShift.TYPE_OVERRIDE:
for field in fields_to_update_for_overrides:
value = None
if field == "priority_level":
value = 0
elif field == "rotation_start":
value = validated_data["start"]
validated_data[field] = value

self._validate_frequency(
validated_data.get("frequency"),
event_type,
validated_data.get("rolling_users"),
validated_data.get("interval"),
validated_data.get("by_day"),
)
self._validate_rotation_start(validated_data["start"], validated_data["rotation_start"])
self._validate_until(validated_data["rotation_start"], validated_data.get("until"))

# convert shift_end into internal value and validate
raw_shift_end = self.initial_data["shift_end"]
shift_end = serializers.DateTimeField().to_internal_value(raw_shift_end)
self._validate_shift_end(validated_data["start"], shift_end)

validated_data["duration"] = shift_end - validated_data["start"]
if validated_data.get("schedule"):
validated_data["team"] = validated_data["schedule"].team

return validated_data

def create(self, validated_data):
validated_data = self._correct_validated_data(validated_data["type"], validated_data)

instance = super().create(validated_data)

instance.start_drop_ical_and_check_schedule_tasks(instance.schedule)
return instance


class OnCallShiftUpdateSerializer(OnCallShiftSerializer):
schedule = serializers.CharField(read_only=True, source="schedule.public_primary_key")
type = serializers.ReadOnlyField()

class Meta(OnCallShiftSerializer.Meta):
read_only_fields = ("schedule", "type")

def update(self, instance, validated_data):
validated_data = self._correct_validated_data(instance.type, validated_data)

result = super().update(instance, validated_data)

instance.start_drop_ical_and_check_schedule_tasks(instance.schedule)
return result
6 changes: 5 additions & 1 deletion engine/apps/api/tests/test_schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,8 +397,10 @@ def test_events_calendar(
name="test_calendar_schedule",
)

start_date = timezone.now().replace(microsecond=0)
data = {
"start": timezone.now().replace(microsecond=0),
"start": start_date,
"rotation_start": start_date,
"duration": timezone.timedelta(seconds=7200),
"priority_level": 2,
}
Expand Down Expand Up @@ -460,6 +462,7 @@ def test_filter_events_calendar(
start_date = now - timezone.timedelta(days=7)
data = {
"start": start_date,
"rotation_start": start_date,
"duration": timezone.timedelta(seconds=7200),
"priority_level": 1,
"frequency": CustomOnCallShift.FREQUENCY_WEEKLY,
Expand Down Expand Up @@ -539,6 +542,7 @@ def test_filter_events_range_calendar(
start_date = now - timezone.timedelta(days=7)
data = {
"start": start_date,
"rotation_start": start_date,
"duration": timezone.timedelta(seconds=7200),
"priority_level": 1,
"frequency": CustomOnCallShift.FREQUENCY_WEEKLY,
Expand Down
2 changes: 2 additions & 0 deletions engine/apps/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .views.integration_heartbeat import IntegrationHeartBeatView
from .views.live_setting import LiveSettingViewSet
from .views.maintenance import MaintenanceAPIView, MaintenanceStartAPIView, MaintenanceStopAPIView
from .views.on_call_shifts import OnCallShiftView
from .views.organization import (
CurrentOrganizationView,
GetChannelVerificationCode,
Expand Down Expand Up @@ -65,6 +66,7 @@
router.register(r"organization_logs", OrganizationLogRecordView, basename="organization_log")
router.register(r"tokens", PublicApiTokenView, basename="api_token")
router.register(r"live_settings", LiveSettingViewSet, basename="live_settings")
router.register(r"oncall_shifts", OnCallShiftView, basename="oncall_shifts")

if settings.MOBILE_APP_PUSH_NOTIFICATIONS_ENABLED:
router.register(r"device/apns", APNSDeviceAuthorizedViewSet)
Expand Down
Loading

0 comments on commit 4520f9f

Please sign in to comment.