Skip to content

add POST /escalation public API endpoint + add public API docs for teams/organization endpoints #4815

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

Merged
merged 11 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
132 changes: 132 additions & 0 deletions docs/sources/oncall-api-reference/direct_paging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/direct_paging/
title: Direct paging HTTP API
weight: 1200
refs:
users:
- pattern: /docs/oncall/
destination: /docs/oncall/<ONCALL_VERSION>/oncall-api-reference/users
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/oncall/oncall-api-reference/users
teams:
- pattern: /docs/oncall/
destination: /docs/oncall/<ONCALL_VERSION>/oncall-api-reference/teams
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/oncall/oncall-api-reference/teams
---

# Direct paging HTTP API

## Directly page a set of users

For more details about how to fetch a user's Grafana OnCall ID, refer to the [Users](ref:users) public API documentation.

```shell
curl "{{API_URL}}/api/v1/direct_paging/" \
--request POST \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json" \
--data '{
"title": "We are seeing a network outage in the datacenter",
"message": "I need help investigating, can you join the investigation?",
"source_url": "https://github.com/myorg/myrepo/issues/123",
"users": [
{
"id": "U281SN24AVVJX",
"important": false
},
{
"id": "U5AKCVNDEDUE7",
"important": true
}
]
}'
```

The above command returns JSON structured in the following way:

```json
{
"alert_group_id": "IZHCC4GTNPZ93"
}
```

## Directly page a team

For more details about how to fetch a team's Grafana OnCall ID, refer to the [Teams](ref:teams) public API documentation.

```shell
curl "{{API_URL}}/api/v1/direct_paging/" \
--request POST \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json" \
--data '{
"title": "We are seeing a network outage in the datacenter",
"message": "I need help investigating, can you join the investigation?",
"source_url": "https://github.com/myorg/myrepo/issues/123",
"team": "TI73TDU19W48J"
}'
```

The above command returns JSON structured in the following way:

```json
{
"alert_group_id": "IZHCC4GTNPZ93"
}
```

## Directly page user(s) for an existing Alert Group

The following shows how you can directly page user(s) for an existing Alert Group.

```shell
curl "{{API_URL}}/api/v1/direct_paging/" \
--request POST \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json" \
--data '{
"alert_group_id": "IZMRNNY8RFS94",
"users": [
{
"id": "U281SN24AVVJX",
"important": false
},
{
"id": "U5AKCVNDEDUE7",
"important": true
}
]
}'
```

The above command returns JSON structured in the following way:

```json
{
"alert_group_id": "IZMRNNY8RFS94"
}
```

| Parameter | Unique | Required | Description |
| -------------------- | :----: | :--------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `title` | No | No | Name of the Alert Group that will be created |
| `message` | No | No | Content of the Alert Group that will be created |
| `source_url` | No | No | Value that will be added in the Alert's payload as `oncall.permalink`. This can be useful to have the source URL/button autopopulated with a URL of interest. |
| `team` | No | Yes (see [Things to Note](#things-to-note)) | Grafana OnCall team ID. If specified, will use the Direct Paging Integration associated with this Grafana OnCall team, to create the Direct Paging Alert Group. |
| `users` | No | Yes (see [Things to Note](#things-to-note)) | List of user(s) to Direct Page. See above request example for object schema. `id` represents the Grafana OnCall user's ID. `important` is a boolean representing whether to escalate the Alert Group using this user's default or important personal notification policy. |
| `alert_group_id` | No | No | If specified, will directly page the specified users for this Alert Group. |

## Things to note

- `team` and `users` are mutually exclusive in the request payload. If you would like to directly page a team AND user(s),
first directly page a team, then using the Alert Group ID returned in response payload, add the required users to the
existing Alert Group
- `alert_group_id` is mutually exclusive with `title`, `message`, and `source_url`. Practically speaking this means that
if you are trying to directly page users on an existing Alert Group, you cannot update the `title`, `message`, or
`source_url` of that Alert Group
- If directly paging users for an existing Alert Group, the Alert Group cannot be in a resolved state

**HTTP request**

`POST {{API_URL}}/api/v1/direct_paging/`
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ def validate(self, attrs):
return attrs


class DirectPagingSerializer(serializers.Serializer):
class BasePagingSerializer(serializers.Serializer):
context: SerializerContext

ALLOWS_GRAFANA_INCIDENT_ID = False

users = UserReferenceSerializer(many=True, required=False, default=list)
team = TeamPrimaryKeyRelatedField(allow_null=True, default=CurrentTeamDefault())

Expand All @@ -44,21 +46,24 @@ class DirectPagingSerializer(serializers.Serializer):
title = serializers.CharField(required=False, default=None)
message = serializers.CharField(required=False, default=None, allow_null=True)
source_url = serializers.URLField(required=False, default=None, allow_null=True)
grafana_incident_id = serializers.CharField(required=False, default=None, allow_null=True)

def validate(self, attrs):
organization = self.context["organization"]
alert_group_id = attrs["alert_group_id"]
title = attrs["title"]
message = attrs["message"]
source_url = attrs["source_url"]
grafana_incident_id = attrs["grafana_incident_id"]
grafana_incident_id = self.ALLOWS_GRAFANA_INCIDENT_ID and attrs.get("grafana_incident_id")

if alert_group_id and (title or message or source_url or grafana_incident_id):
raise serializers.ValidationError(
"alert_group_id and (title, message, source_url, grafana_incident_id) are mutually exclusive"
f"alert_group_id and (title, message, source_url{', grafana_incident_id' if self.ALLOWS_GRAFANA_INCIDENT_ID else ''}) "
"are mutually exclusive"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is here because the grafana_incident_id is really only a concept that is relevant for the internal API (you cannot specify this attribute from the public API)

)

if attrs["users"] and attrs["team"]:
raise serializers.ValidationError("users and team are mutually exclusive")
Comment on lines +64 to +65
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is already the case if you perform this workflow from Slack or the web plugin UI, you cannot specify users and a team, we just weren't previously validating the payload for it. Added test cases for this:


if alert_group_id:
try:
attrs["alert_group"] = AlertGroup.objects.get(
Expand All @@ -68,3 +73,9 @@ def validate(self, attrs):
raise serializers.ValidationError("Alert group {} does not exist".format(alert_group_id))

return attrs


class DirectPagingSerializer(BasePagingSerializer):
ALLOWS_GRAFANA_INCIDENT_ID = True

grafana_incident_id = serializers.CharField(required=False, default=None, allow_null=True)
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,41 @@ def test_direct_paging_no_user_or_team_specified(
assert response.json()["detail"] == DirectPagingUserTeamValidationError.DETAIL


@pytest.mark.django_db
def test_direct_paging_both_team_and_users_specified(
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
make_user,
make_team,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR)
team = make_team(organization=organization)

# user must be part of the team
user.teams.add(team)

client = APIClient()
url = reverse("api-internal:direct_paging")

response = client.post(
url,
data={
"team": team.public_primary_key,
"users": [
{
"id": make_user(organization=organization).public_primary_key,
"important": False,
},
],
},
format="json",
**make_user_auth_headers(user, token),
)

assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json()["non_field_errors"] == ["users and team are mutually exclusive"]


@pytest.mark.parametrize(
"field_name,field_value",
[
Expand Down
2 changes: 1 addition & 1 deletion engine/apps/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .views.alert_receive_channel_template import AlertReceiveChannelTemplateView
from .views.alerts import AlertDetailView
from .views.channel_filter import ChannelFilterView
from .views.direct_paging import DirectPagingAPIView
from .views.escalation_chain import EscalationChainViewSet
from .views.escalation_policy import EscalationPolicyView
from .views.features import FeaturesAPIView
Expand All @@ -23,7 +24,6 @@
OrganizationConfigChecksView,
SetGeneralChannel,
)
from .views.paging import DirectPagingAPIView
from .views.preview_template_options import PreviewTemplateOptionsView
from .views.public_api_tokens import PublicApiTokenView
from .views.resolution_note import ResolutionNoteView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from apps.alerts.paging import DirectPagingAlertGroupResolvedError, DirectPagingUserTeamValidationError, direct_paging
from apps.api.permissions import RBACPermission
from apps.api.serializers.paging import DirectPagingSerializer
from apps.api.serializers.direct_paging import DirectPagingSerializer
from apps.auth_token.auth import PluginAuthentication
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication
from common.api_helpers.exceptions import BadRequest
Expand Down
8 changes: 8 additions & 0 deletions engine/apps/public_api/serializers/direct_paging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from apps.api.serializers.direct_paging import BasePagingSerializer


class DirectPagingSerializer(BasePagingSerializer):
"""
Very similar to `apps.api.serializers.direct_paging.DirectPagingSerializer` except that
there is no `grafana_incident_id` attribute
"""
Loading
Loading