Skip to content

Commit 4520f9f

Browse files
Merge pull request #278 from grafana/dev
Merge dev to main
2 parents 7627853 + c9c6df8 commit 4520f9f

17 files changed

+478
-45
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Change Log
22

3+
## v1.0.10 (2022-07-22)
4+
- Speed-up of alert group web caching
5+
- Internal api for OnCall shifts
6+
37
## v1.0.9 (2022-07-21)
48
- Frontend bug fixes & improvements
59
- Support regex_replace() in templates

engine/apps/alerts/models/alert_receive_channel.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -694,14 +694,19 @@ def listen_for_alertreceivechannel_model_save(sender, instance, created, *args,
694694
instance.organization, None, OrganizationLogType.TYPE_HEARTBEAT_CREATED, description
695695
)
696696
else:
697-
logger.info(f"Drop AG cache. Reason: save alert_receive_channel {instance.pk}")
698697
if kwargs is not None:
699698
if "update_fields" in kwargs:
700699
if kwargs["update_fields"] is not None:
700+
fields_to_not_to_invalidate_cache = [
701+
"rate_limit_message_task_id",
702+
"rate_limited_in_slack_at",
703+
"reason_to_skip_escalation",
704+
]
701705
# Hack to not to invalidate web cache on AlertReceiveChannel.start_send_rate_limit_message_task
702-
if "rate_limit_message_task_id" in kwargs["update_fields"]:
703-
return
704-
706+
for f in fields_to_not_to_invalidate_cache:
707+
if f in kwargs["update_fields"]:
708+
return
709+
logger.info(f"Drop AG cache. Reason: save alert_receive_channel {instance.pk}")
705710
invalidate_web_cache_for_alert_group.apply_async(kwargs={"channel_pk": instance.pk})
706711

707712
if instance.integration == AlertReceiveChannel.INTEGRATION_GRAFANA_ALERTING:

engine/apps/alerts/tests/test_escalation_policy_snapshot.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,10 @@ def test_escalation_step_notify_on_call_schedule(
170170

171171
schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar)
172172
# create on_call_shift with user to notify
173+
start_date = timezone.datetime.now().replace(microsecond=0)
173174
data = {
174-
"start": timezone.datetime.now().replace(microsecond=0),
175+
"start": start_date,
176+
"rotation_start": start_date,
175177
"duration": timezone.timedelta(seconds=7200),
176178
}
177179
on_call_shift = make_on_call_shift(
@@ -216,8 +218,10 @@ def test_escalation_step_notify_on_call_schedule_viewer_user(
216218

217219
schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar)
218220
# create on_call_shift with user to notify
221+
start_date = timezone.datetime.now().replace(microsecond=0)
219222
data = {
220-
"start": timezone.datetime.now().replace(microsecond=0),
223+
"start": start_date,
224+
"rotation_start": start_date,
221225
"duration": timezone.timedelta(seconds=7200),
222226
}
223227
on_call_shift = make_on_call_shift(

engine/apps/alerts/tests/test_terraform_renderer.py

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def test_render_terraform_file(
9999
interval=1,
100100
week_start=CustomOnCallShift.MONDAY,
101101
start=dateparse.parse_datetime("2021-08-16T17:00:00"),
102+
rotation_start=dateparse.parse_datetime("2021-08-16T17:00:00"),
102103
duration=timezone.timedelta(seconds=3600),
103104
by_day=["MO", "SA"],
104105
rolling_users=[{user.pk: user.public_primary_key}],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
from rest_framework import serializers
2+
3+
from apps.schedules.models import CustomOnCallShift, OnCallScheduleWeb
4+
from apps.user_management.models import User
5+
from common.api_helpers.custom_fields import (
6+
OrganizationFilteredPrimaryKeyRelatedField,
7+
RollingUsersField,
8+
UsersFilteredByOrganizationField,
9+
)
10+
from common.api_helpers.mixins import EagerLoadingMixin
11+
from common.api_helpers.utils import CurrentOrganizationDefault
12+
13+
14+
class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer):
15+
id = serializers.CharField(read_only=True, source="public_primary_key")
16+
organization = serializers.HiddenField(default=CurrentOrganizationDefault())
17+
type = serializers.ChoiceField(
18+
required=True,
19+
choices=CustomOnCallShift.WEB_TYPES,
20+
)
21+
schedule = OrganizationFilteredPrimaryKeyRelatedField(queryset=OnCallScheduleWeb.objects)
22+
frequency = serializers.ChoiceField(required=False, choices=CustomOnCallShift.FREQUENCY_CHOICES, allow_null=True)
23+
shift_start = serializers.DateTimeField(source="start")
24+
shift_end = serializers.SerializerMethodField()
25+
by_day = serializers.ListField(required=False, allow_null=True)
26+
rolling_users = RollingUsersField(
27+
allow_null=True,
28+
required=False,
29+
child=UsersFilteredByOrganizationField(
30+
queryset=User.objects, required=False, allow_null=True
31+
), # todo: filter by team?
32+
)
33+
34+
class Meta:
35+
model = CustomOnCallShift
36+
fields = [
37+
"id",
38+
"organization",
39+
"name",
40+
"type",
41+
"schedule",
42+
"priority_level",
43+
"shift_start",
44+
"shift_end",
45+
"rotation_start",
46+
"until",
47+
"frequency",
48+
"interval",
49+
"by_day",
50+
"source",
51+
"rolling_users",
52+
]
53+
extra_kwargs = {
54+
"interval": {"required": False, "allow_null": True},
55+
"source": {"required": False, "write_only": True},
56+
}
57+
58+
SELECT_RELATED = ["schedule"]
59+
60+
def get_shift_end(self, obj):
61+
return obj.start + obj.duration
62+
63+
def to_internal_value(self, data):
64+
data["source"] = CustomOnCallShift.SOURCE_WEB
65+
data["week_start"] = CustomOnCallShift.MONDAY
66+
if not data.get("shift_end"):
67+
raise serializers.ValidationError({"shift_end": ["This field is required."]})
68+
69+
result = super().to_internal_value(data)
70+
return result
71+
72+
def to_representation(self, instance):
73+
result = super().to_representation(instance)
74+
return result
75+
76+
def validate_name(self, name):
77+
organization = self.context["request"].auth.organization
78+
if name is None:
79+
return name
80+
try:
81+
obj = CustomOnCallShift.objects.get(organization=organization, name=name)
82+
except CustomOnCallShift.DoesNotExist:
83+
return name
84+
if self.instance and obj.id == self.instance.id:
85+
return name
86+
else:
87+
raise serializers.ValidationError(["On-call shift with this name already exists"])
88+
89+
def validate_by_day(self, by_day):
90+
if by_day:
91+
for day in by_day:
92+
if day not in CustomOnCallShift.WEB_WEEKDAY_MAP:
93+
raise serializers.ValidationError(["Invalid day value."])
94+
return by_day
95+
96+
def validate_interval(self, interval):
97+
if interval is not None:
98+
if not isinstance(interval, int) or interval <= 0:
99+
raise serializers.ValidationError(["Invalid value"])
100+
return interval
101+
102+
def validate_rolling_users(self, rolling_users):
103+
result = []
104+
if rolling_users:
105+
for users in rolling_users:
106+
users_dict = dict()
107+
for user in users:
108+
users_dict[user.pk] = user.public_primary_key
109+
result.append(users_dict)
110+
return result
111+
112+
def _validate_shift_end(self, start, end):
113+
if end <= start:
114+
raise serializers.ValidationError({"shift_end": ["Incorrect shift end date"]})
115+
116+
def _validate_frequency(self, frequency, event_type, rolling_users, interval, by_day):
117+
if frequency is None:
118+
if rolling_users and len(rolling_users) > 1:
119+
raise serializers.ValidationError(
120+
{"rolling_users": ["Cannot set multiple user groups for non-recurrent shifts"]}
121+
)
122+
if interval is not None:
123+
raise serializers.ValidationError({"interval": ["Cannot set interval for non-recurrent shifts"]})
124+
if by_day:
125+
raise serializers.ValidationError({"by_day": ["Cannot set days value for non-recurrent shifts"]})
126+
else:
127+
if event_type == CustomOnCallShift.TYPE_OVERRIDE:
128+
raise serializers.ValidationError(
129+
{"frequency": ["Cannot set 'frequency' for shifts with type 'override'"]}
130+
)
131+
if frequency != CustomOnCallShift.FREQUENCY_WEEKLY and by_day:
132+
raise serializers.ValidationError({"by_day": ["Cannot set days value for this frequency type"]})
133+
134+
def _validate_rotation_start(self, shift_start, rotation_start):
135+
if rotation_start < shift_start:
136+
raise serializers.ValidationError({"rotation_start": ["Incorrect rotation start date"]})
137+
138+
def _validate_until(self, rotation_start, until):
139+
if until is not None and until < rotation_start:
140+
raise serializers.ValidationError({"until": ["Incorrect rotation end date"]})
141+
142+
def _correct_validated_data(self, event_type, validated_data):
143+
fields_to_update_for_overrides = [
144+
"priority_level",
145+
"frequency",
146+
"interval",
147+
"by_day",
148+
"until",
149+
"rotation_start",
150+
]
151+
if event_type == CustomOnCallShift.TYPE_OVERRIDE:
152+
for field in fields_to_update_for_overrides:
153+
value = None
154+
if field == "priority_level":
155+
value = 0
156+
elif field == "rotation_start":
157+
value = validated_data["start"]
158+
validated_data[field] = value
159+
160+
self._validate_frequency(
161+
validated_data.get("frequency"),
162+
event_type,
163+
validated_data.get("rolling_users"),
164+
validated_data.get("interval"),
165+
validated_data.get("by_day"),
166+
)
167+
self._validate_rotation_start(validated_data["start"], validated_data["rotation_start"])
168+
self._validate_until(validated_data["rotation_start"], validated_data.get("until"))
169+
170+
# convert shift_end into internal value and validate
171+
raw_shift_end = self.initial_data["shift_end"]
172+
shift_end = serializers.DateTimeField().to_internal_value(raw_shift_end)
173+
self._validate_shift_end(validated_data["start"], shift_end)
174+
175+
validated_data["duration"] = shift_end - validated_data["start"]
176+
if validated_data.get("schedule"):
177+
validated_data["team"] = validated_data["schedule"].team
178+
179+
return validated_data
180+
181+
def create(self, validated_data):
182+
validated_data = self._correct_validated_data(validated_data["type"], validated_data)
183+
184+
instance = super().create(validated_data)
185+
186+
instance.start_drop_ical_and_check_schedule_tasks(instance.schedule)
187+
return instance
188+
189+
190+
class OnCallShiftUpdateSerializer(OnCallShiftSerializer):
191+
schedule = serializers.CharField(read_only=True, source="schedule.public_primary_key")
192+
type = serializers.ReadOnlyField()
193+
194+
class Meta(OnCallShiftSerializer.Meta):
195+
read_only_fields = ("schedule", "type")
196+
197+
def update(self, instance, validated_data):
198+
validated_data = self._correct_validated_data(instance.type, validated_data)
199+
200+
result = super().update(instance, validated_data)
201+
202+
instance.start_drop_ical_and_check_schedule_tasks(instance.schedule)
203+
return result

engine/apps/api/tests/test_schedules.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -397,8 +397,10 @@ def test_events_calendar(
397397
name="test_calendar_schedule",
398398
)
399399

400+
start_date = timezone.now().replace(microsecond=0)
400401
data = {
401-
"start": timezone.now().replace(microsecond=0),
402+
"start": start_date,
403+
"rotation_start": start_date,
402404
"duration": timezone.timedelta(seconds=7200),
403405
"priority_level": 2,
404406
}
@@ -460,6 +462,7 @@ def test_filter_events_calendar(
460462
start_date = now - timezone.timedelta(days=7)
461463
data = {
462464
"start": start_date,
465+
"rotation_start": start_date,
463466
"duration": timezone.timedelta(seconds=7200),
464467
"priority_level": 1,
465468
"frequency": CustomOnCallShift.FREQUENCY_WEEKLY,
@@ -539,6 +542,7 @@ def test_filter_events_range_calendar(
539542
start_date = now - timezone.timedelta(days=7)
540543
data = {
541544
"start": start_date,
545+
"rotation_start": start_date,
542546
"duration": timezone.timedelta(seconds=7200),
543547
"priority_level": 1,
544548
"frequency": CustomOnCallShift.FREQUENCY_WEEKLY,

engine/apps/api/urls.py

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from .views.integration_heartbeat import IntegrationHeartBeatView
1818
from .views.live_setting import LiveSettingViewSet
1919
from .views.maintenance import MaintenanceAPIView, MaintenanceStartAPIView, MaintenanceStopAPIView
20+
from .views.on_call_shifts import OnCallShiftView
2021
from .views.organization import (
2122
CurrentOrganizationView,
2223
GetChannelVerificationCode,
@@ -65,6 +66,7 @@
6566
router.register(r"organization_logs", OrganizationLogRecordView, basename="organization_log")
6667
router.register(r"tokens", PublicApiTokenView, basename="api_token")
6768
router.register(r"live_settings", LiveSettingViewSet, basename="live_settings")
69+
router.register(r"oncall_shifts", OnCallShiftView, basename="oncall_shifts")
6870

6971
if settings.MOBILE_APP_PUSH_NOTIFICATIONS_ENABLED:
7072
router.register(r"device/apns", APNSDeviceAuthorizedViewSet)

0 commit comments

Comments
 (0)