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

Webhooks Public API #2790

Merged
merged 10 commits into from
Aug 22, 2023
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Added

- Public API for webhooks @mderynck ([#2790](https://github.com/grafana/oncall/pull/2790))

### Changed

- Public API for actions now wraps webhooks @mderynck ([#2790](https://github.com/grafana/oncall/pull/2790))

## v1.3.25 (2023-08-18)

### Changed
Expand Down
194 changes: 183 additions & 11 deletions docs/sources/oncall-api-reference/outgoing_webhooks.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
---
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/outgoing_webhooks/
title: Outgoing webhooks HTTP API
title: Outgoing Webhooks HTTP API
weight: 700
---

# Outgoing webhooks (actions)
# Outgoing Webhooks

Used in escalation policies with type `trigger_action`.
> ⚠️ A note about actions: Before version **v1.3.11** webhooks existed as actions within the API, the /actions
> endpoint remains available and is compatible with previous callers but under the hood it will interact with the
> new webhooks objects. It is recommended to use the /webhooks endpoint going forward which has more features.

## List actions
For more details about specific fields of a webhook see [outgoing webhooks][outgoing-webhooks] documentation.

## List webhooks

```shell
curl "{{API_URL}}/api/v1/actions/" \
curl "{{API_URL}}/api/v1/webhooks/" \
--request GET \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json"
Expand All @@ -21,21 +25,189 @@ The above command returns JSON structured in the following way:

```json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": "KGEFG74LU1D8L",
"name": "Publish alert group notification to JIRA"
"id": "{{WEBHOOK_UID}}",
"name": "Demo Webhook",
"is_webhook_enabled": true,
"team": null,
"data": "{\"labels\" : {{ alert_payload.commonLabels | tojson()}}}",
"username": null,
"password": null,
"authorization_header": "****************",
"trigger_template": null,
"headers": null,
"url": "https://example.com",
"forward_all": false,
"http_method": "POST",
"trigger_type": "acknowledge",
"integration_filter": [
"CRV8A5MXC751A"
]
}
],
"current_page_number": 1,
"page_size": 50,
"count": 1,
"current_page_number": 1,
"total_pages": 1
}
```

**HTTP request**
## Get webhook

```shell
curl "{{API_URL}}/api/v1/webhooks/{{WEBHOOK_UID}}/" \
--request GET \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json"
```

The above command returns JSON structured in the following way:

```json
{
"id": "{{WEBHOOK_UID}}",
"name": "Demo Webhook",
"is_webhook_enabled": true,
"team": null,
"data": "{\"labels\" : {{ alert_payload.commonLabels | tojson()}}}",
"username": null,
"password": null,
"authorization_header": "****************",
"trigger_template": null,
"headers": null,
"url": "https://example.com",
"forward_all": false,
"http_method": "POST",
"trigger_type": "acknowledge",
"integration_filter": [
"CRV8A5MXC751A"
]
}
```

## Create webhook

```shell
curl "{{API_URL}}/api/v1/webhooks/" \
--request POST \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json" \
--data '{
"name": "New Webhook",
"url": "https://example.com",
"http_method": "POST",
"trigger_type" : "resolve"
mderynck marked this conversation as resolved.
Show resolved Hide resolved
}'
```

The above command returns JSON structured in the following way:

```json
{
"id": "{{WEBHOOK_UID}}",
"name": "New Webhook",
"is_webhook_enabled": true,
"team": null,
"data": null,
"username": null,
"password": null,
"authorization_header": null,
"trigger_template": null,
"headers": null,
"url": "https://example.com",
"forward_all": true,
"http_method": "POST",
"trigger_type": "resolve",
"integration_filter": null
}
```

## Update webhook

`GET {{API_URL}}/api/v1/actions/`
```shell
curl "{{API_URL}}/api/v1/webhooks/{{WEBHOOK_UID}}/" \
--request PUT \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json" \
--data '{
"is_webhook_enabled": false
}'
```

The above command returns JSON structured in the following way:

```json
{
"id": "{{WEBHOOK_UID}}",
"name": "New Webhook",
"is_webhook_enabled": false,
"team": null,
"data": null,
"username": null,
"password": null,
"authorization_header": null,
"trigger_template": null,
"headers": null,
"url": "https://example.com",
"forward_all": true,
"http_method": "POST",
"trigger_type": "resolve",
"integration_filter": null
}
```

## Delete webhook

```shell
curl "{{API_URL}}/api/v1/webhooks/{{WEBHOOK_UID}}/" \
--request DELETE \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json"
```

## Get webhook responses

```shell
curl "{{API_URL}}/api/v1/webhooks/{{WEBHOOK_UID}}/responses" \
--request GET \
--header "Authorization: meowmeowmeow" \
--header "Content-Type: application/json"
```

The above command returns JSON structured in the following way:

```json
{
"next": null,
"previous": null,
"results": [
{
"timestamp": "2023-08-18T16:38:23.106015Z",
"url": "https://example.com",
"request_trigger": "",
"request_headers": "{\"Authorization\": \"****************\"}",
"request_data": "{\"labels\": {\"alertname\": \"InstanceDown\", \"job\": \"node\", \"severity\": \"critical\"}}",
"status_code": 200,
"content": "",
"event_data": "{\"event\": {\"type\": \"acknowledge\", \"time\": \"2023-08-18T16:38:21.442981+00:00\"}, \"user\": {\"id\": \"UK49JJNPZMFLJ\", \"username\": \"oncall\", \"email\": \"admin@localhost\"}, \"alert_group\": {\"id\": \"IZQERPWKWCGH1\", \"integration_id\": \"CRV8A5MXC751A\", \"route_id\": \"RWNCT6C77M3WM\", \"alerts_count\": 1, \"state\": \"acknowledged\", \"created_at\": \"2023-08-18T16:34:27.678406Z\", \"resolved_at\": null, \"acknowledged_at\": \"2023-08-18T16:38:21.442981Z\", \"title\": \"[firing:2] InstanceDown \", \"permalinks\": {\"slack\": null, \"telegram\": null, \"web\": \"http://localhost:3000/a/grafana-oncall-app/alert-groups/IZQERPWKWCGH1\"}}, \"alert_group_id\": \"IZQERPWKWCGH1\", \"alert_payload\": {\"alerts\": [{\"endsAt\": \"0001-01-01T00:00:00Z\", \"labels\": {\"job\": \"node\", \"group\": \"production\", \"instance\": \"localhost:8081\", \"severity\": \"critical\", \"alertname\": \"InstanceDown\"}, \"status\": \"firing\", \"startsAt\": \"2023-06-12T08:24:38.326Z\", \"annotations\": {\"title\": \"Instance localhost:8081 down\", \"description\": \"localhost:8081 of job node has been down for more than 1 minute.\"}, \"fingerprint\": \"f404ecabc8dd5cd7\", \"generatorURL\": \"\"}, {\"endsAt\": \"0001-01-01T00:00:00Z\", \"labels\": {\"job\": \"node\", \"group\": \"canary\", \"instance\": \"localhost:8082\", \"severity\": \"critical\", \"alertname\": \"InstanceDown\"}, \"status\": \"firing\", \"startsAt\": \"2023-06-12T08:24:38.326Z\", \"annotations\": {\"title\": \"Instance localhost:8082 down\", \"description\": \"localhost:8082 of job node has been down for more than 1 minute.\"}, \"fingerprint\": \"f8f08d4e32c61a9d\", \"generatorURL\": \"\"}], \"status\": \"firing\", \"version\": \"4\", \"groupKey\": \"{}:{alertname=\\\"InstanceDown\\\"}\", \"receiver\": \"combo\", \"numFiring\": 2, \"externalURL\": \"\", \"groupLabels\": {\"alertname\": \"InstanceDown\"}, \"numResolved\": 0, \"commonLabels\": {\"job\": \"node\", \"severity\": \"critical\", \"alertname\": \"InstanceDown\"}, \"truncatedAlerts\": 0, \"commonAnnotations\": {}}, \"integration\": {\"id\": \"CRV8A5MXC751A\", \"type\": \"alertmanager\", \"name\": \"One - Alertmanager\", \"team\": null}, \"notified_users\": [], \"users_to_be_notified\": []}"
},
{
"timestamp": "2023-08-18T16:34:38.580574Z",
"url": "https://example.com",
"request_trigger": "",
"request_headers": null,
"request_data": "Data - Template Warning: Object of type Undefined is not JSON serializable",
"status_code": null,
"content": null,
"event_data": "{\"event\": {\"type\": \"acknowledge\", \"time\": \"2023-08-18T16:34:37.940655+00:00\"}, \"user\": {\"id\": \"UK49JJNPZMFLJ\", \"username\": \"oncall\", \"email\": \"admin@localhost\"}, \"alert_group\": {\"id\": \"IZQERPWKWCGH1\", \"integration_id\": \"CRV8A5MXC751A\", \"route_id\": \"RWNCT6C77M3WM\", \"alerts_count\": 1, \"state\": \"acknowledged\", \"created_at\": \"2023-08-18T16:34:27.678406Z\", \"resolved_at\": null, \"acknowledged_at\": \"2023-08-18T16:34:37.940655Z\", \"title\": \"[firing:2] InstanceDown \", \"permalinks\": {\"slack\": null, \"telegram\": null, \"web\": \"http://localhost:3000/a/grafana-oncall-app/alert-groups/IZQERPWKWCGH1\"}}, \"alert_group_id\": \"IZQERPWKWCGH1\", \"alert_payload\": {\"alerts\": [{\"endsAt\": \"0001-01-01T00:00:00Z\", \"labels\": {\"job\": \"node\", \"group\": \"production\", \"instance\": \"localhost:8081\", \"severity\": \"critical\", \"alertname\": \"InstanceDown\"}, \"status\": \"firing\", \"startsAt\": \"2023-06-12T08:24:38.326Z\", \"annotations\": {\"title\": \"Instance localhost:8081 down\", \"description\": \"localhost:8081 of job node has been down for more than 1 minute.\"}, \"fingerprint\": \"f404ecabc8dd5cd7\", \"generatorURL\": \"\"}, {\"endsAt\": \"0001-01-01T00:00:00Z\", \"labels\": {\"job\": \"node\", \"group\": \"canary\", \"instance\": \"localhost:8082\", \"severity\": \"critical\", \"alertname\": \"InstanceDown\"}, \"status\": \"firing\", \"startsAt\": \"2023-06-12T08:24:38.326Z\", \"annotations\": {\"title\": \"Instance localhost:8082 down\", \"description\": \"localhost:8082 of job node has been down for more than 1 minute.\"}, \"fingerprint\": \"f8f08d4e32c61a9d\", \"generatorURL\": \"\"}], \"status\": \"firing\", \"version\": \"4\", \"groupKey\": \"{}:{alertname=\\\"InstanceDown\\\"}\", \"receiver\": \"combo\", \"numFiring\": 2, \"externalURL\": \"\", \"groupLabels\": {\"alertname\": \"InstanceDown\"}, \"numResolved\": 0, \"commonLabels\": {\"job\": \"node\", \"severity\": \"critical\", \"alertname\": \"InstanceDown\"}, \"truncatedAlerts\": 0, \"commonAnnotations\": {}}, \"integration\": {\"id\": \"CRV8A5MXC751A\", \"type\": \"alertmanager\", \"name\": \"One - Alertmanager\", \"team\": null}, \"notified_users\": [], \"users_to_be_notified\": []}"
}
],
"page_size": 50,
"count": 2,
"current_page_number": 1,
"total_pages": 1
}
```
2 changes: 1 addition & 1 deletion docs/sources/outgoing-webhooks/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ To fix change the template to:

```json
{
"labels": "{{ alert_payload.labels | tojson()}}"
"labels": {{ alert_payload.labels | tojson()}}
}
```

Expand Down
1 change: 0 additions & 1 deletion engine/apps/api/serializers/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ class Meta:
"is_webhook_enabled",
"is_legacy",
"team",
"data",
"user",
"username",
"password",
Expand Down
8 changes: 4 additions & 4 deletions engine/apps/api/tests/test_webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ def test_get_list_webhooks(webhook_internal_api_setup, make_user_auth_headers):
"event_data": "",
},
"trigger_template": None,
"trigger_type": None,
"trigger_type_name": "",
"trigger_type": "0",
"trigger_type_name": "Escalation step",
}
]

Expand Down Expand Up @@ -106,8 +106,8 @@ def test_get_detail_webhook(webhook_internal_api_setup, make_user_auth_headers):
"event_data": "",
},
"trigger_template": None,
"trigger_type": None,
"trigger_type_name": "",
"trigger_type": "0",
"trigger_type_name": "Escalation step",
}

response = client.get(url, format="json", **make_user_auth_headers(user, token))
Expand Down
25 changes: 25 additions & 0 deletions engine/apps/api/views/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from common.api_helpers.exceptions import BadRequest
from common.api_helpers.filters import ByTeamModelFieldFilterMixin, ModelFieldFilterMixin, TeamModelMultipleChoiceFilter
from common.api_helpers.mixins import PublicPrimaryKeyMixin, TeamFilteringMixin
from common.insight_log import EntityEvent, write_resource_insight_log
from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning

NEW_WEBHOOK_PK = "new"
Expand Down Expand Up @@ -60,6 +61,30 @@ class WebhooksView(TeamFilteringMixin, PublicPrimaryKeyMixin, ModelViewSet):
search_fields = ["public_primary_key", "name"]
filterset_class = WebhooksFilter

def perform_create(self, serializer):
serializer.save()
write_resource_insight_log(instance=serializer.instance, author=self.request.user, event=EntityEvent.CREATED)

def perform_update(self, serializer):
prev_state = serializer.instance.insight_logs_serialized
serializer.save()
new_state = serializer.instance.insight_logs_serialized
write_resource_insight_log(
instance=serializer.instance,
author=self.request.user,
event=EntityEvent.UPDATED,
prev_state=prev_state,
new_state=new_state,
)

def perform_destroy(self, instance):
write_resource_insight_log(
instance=instance,
author=self.request.user,
event=EntityEvent.DELETED,
)
instance.delete()

def get_queryset(self, ignore_filtering_by_available_teams=False):
queryset = Webhook.objects.filter(
organization=self.request.auth.organization,
Expand Down
Loading
Loading