diff --git a/engine/apps/api/tests/test_alert_receive_channel.py b/engine/apps/api/tests/test_alert_receive_channel.py index 460c335e26..3831cce17e 100644 --- a/engine/apps/api/tests/test_alert_receive_channel.py +++ b/engine/apps/api/tests/test_alert_receive_channel.py @@ -2719,7 +2719,12 @@ def test_alert_receive_channel_integration_options_search( response = client.get(search_url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK returned_choices = [i["display_name"] for i in response.json()] - assert returned_choices == ["Grafana Alerting", "Grafana Legacy Alerting", "(Deprecated) Grafana Alerting"] + assert returned_choices == [ + "Grafana Alerting", + "Grafana Legacy Alerting", + "(Deprecated) Grafana Alerting", + "Adaptive Grafana Alerting", + ] search_url = f"{url}?search=notfound" response = client.get(search_url, format="json", **make_user_auth_headers(user, token)) diff --git a/engine/apps/grafana_plugin/helpers/client.py b/engine/apps/grafana_plugin/helpers/client.py index 0037cb7410..6fcef8b65a 100644 --- a/engine/apps/grafana_plugin/helpers/client.py +++ b/engine/apps/grafana_plugin/helpers/client.py @@ -351,6 +351,9 @@ def validate_grafana_token_format(grafana_token: str) -> bool: return False return True + def forward_alert(self, alert_payload): + return self.api_post("api/alertmanager/grafana/api/v2/alerts", alert_payload) + class GcomAPIClient(APIClient): ACTIVE_INSTANCE_QUERY = "instances?status=active" diff --git a/engine/apps/integrations/mixins/alert_forwarding_mixin.py b/engine/apps/integrations/mixins/alert_forwarding_mixin.py new file mode 100644 index 0000000000..660c9e3ec7 --- /dev/null +++ b/engine/apps/integrations/mixins/alert_forwarding_mixin.py @@ -0,0 +1,48 @@ +import json +import logging + +from django.http import JsonResponse + +from apps.alerts.models import AlertReceiveChannel +from apps.grafana_plugin.helpers import GrafanaAPIClient + +logger = logging.getLogger(__name__) + + +class AlertForwardingMixin: + def dispatch(self, *args, **kwargs): + if kwargs.get("integration_type") == "elastalert": + token = str(kwargs["alert_channel_key"]) + # TODO: replace with proper caching logic later + alert_receive_channel = AlertReceiveChannel.objects.get(token=token) + organization = alert_receive_channel.organization + if not alert_receive_channel: + return JsonResponse({"error": "Invalid alert receive channel"}, status=400) + + forwarded_payload = [ + { + "labels": {"alertname": "HighLatency", "service": "my-service", "severity": "critical"}, + "annotations": { + "summary": "The service is experiencing unusually high latency.", + "description": "Latency has exceeded 300ms for the last 10 minutes.", + }, + "startsAt": "2024-12-09T10:00:00Z", + "endsAt": "0001-01-01T00:00:00Z", + "generatorURL": "http://my-service.example.com/metrics", + } + ] + try: + data = json.loads(self.body) + # Transform Here + + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + # Forward + client = GrafanaAPIClient(api_url=organization.grafana_url, api_token=organization.api_token) + _, status = client.forward_alert(forwarded_payload) + if status["status_code"] != 200: + return JsonResponse({"status": status}, status=status["status_code"]) + return JsonResponse({"data": data}, status=200) + + return None diff --git a/engine/apps/integrations/tests/test_views.py b/engine/apps/integrations/tests/test_views.py index cd4bc26e66..da6b22ab98 100644 --- a/engine/apps/integrations/tests/test_views.py +++ b/engine/apps/integrations/tests/test_views.py @@ -56,7 +56,16 @@ def setup_failing_redis_cache(settings): [ arc_type for arc_type in INTEGRATION_TYPES - if arc_type not in ["amazon_sns", "grafana", "alertmanager", "grafana_alerting", "maintenance"] + if arc_type + not in [ + "amazon_sns", + "grafana", + "alertmanager", + "grafana_alerting", + "maintenance", + "adaptive_grafana_alerting", + "elastalert", + ] ], ) @pytest.mark.django_db @@ -104,7 +113,16 @@ def test_integration_universal_endpoint( [ arc_type for arc_type in INTEGRATION_TYPES - if arc_type not in ["amazon_sns", "grafana", "alertmanager", "grafana_alerting", "maintenance"] + if arc_type + not in [ + "amazon_sns", + "grafana", + "alertmanager", + "grafana_alerting", + "maintenance", + "adaptive_grafana_alerting", + "elastalert", + ] ], ) @pytest.mark.django_db @@ -246,7 +264,16 @@ def test_integration_old_grafana_endpoint( [ arc_type for arc_type in INTEGRATION_TYPES - if arc_type not in ["amazon_sns", "grafana", "alertmanager", "grafana_alerting", "maintenance"] + if arc_type + not in [ + "amazon_sns", + "grafana", + "alertmanager", + "grafana_alerting", + "maintenance", + "adaptive_grafana_alerting", + "elastalert", + ] ], ) @pytest.mark.django_db @@ -280,7 +307,16 @@ def test_integration_universal_endpoint_not_allow_files( [ arc_type for arc_type in INTEGRATION_TYPES - if arc_type not in ["amazon_sns", "grafana", "alertmanager", "grafana_alerting", "maintenance"] + if arc_type + not in [ + "amazon_sns", + "grafana", + "alertmanager", + "grafana_alerting", + "maintenance", + "adaptive_grafana_alerting", + "elastalert", + ] ], ) @pytest.mark.django_db @@ -383,7 +419,16 @@ def test_integration_grafana_endpoint_without_db_has_alerts( [ arc_type for arc_type in INTEGRATION_TYPES - if arc_type not in ["amazon_sns", "grafana", "alertmanager", "grafana_alerting", "maintenance"] + if arc_type + not in [ + "amazon_sns", + "grafana", + "alertmanager", + "grafana_alerting", + "maintenance", + "adaptive_grafana_alerting", + "elastalert", + ] ], ) @pytest.mark.django_db @@ -483,7 +528,16 @@ def test_integration_grafana_endpoint_without_cache_has_alerts( [ arc_type for arc_type in INTEGRATION_TYPES - if arc_type not in ["amazon_sns", "grafana", "alertmanager", "grafana_alerting", "maintenance"] + if arc_type + not in [ + "amazon_sns", + "grafana", + "alertmanager", + "grafana_alerting", + "maintenance", + "adaptive_grafana_alerting", + "elastalert", + ] ], ) @pytest.mark.django_db @@ -540,7 +594,7 @@ def test_integration_outdated_cached_model( ) @pytest.mark.parametrize( "integration_type", - [arc_type for arc_type in INTEGRATION_TYPES], + [arc_type for arc_type in INTEGRATION_TYPES if arc_type not in ["adaptive_grafana_alerting", "elastalert"]], ) @pytest.mark.django_db def test_non_existent_integration_does_not_repeat_access_db( @@ -576,7 +630,7 @@ def test_non_existent_integration_does_not_repeat_access_db( ) @pytest.mark.parametrize( "integration_type", - [arc_type for arc_type in INTEGRATION_TYPES], + [arc_type for arc_type in INTEGRATION_TYPES if arc_type not in ["adaptive_grafana_alerting", "elastalert"]], ) @pytest.mark.django_db def test_deleted_integration_does_not_repeat_access_db( diff --git a/engine/apps/integrations/urls.py b/engine/apps/integrations/urls.py index f0170510bf..aaaa855488 100644 --- a/engine/apps/integrations/urls.py +++ b/engine/apps/integrations/urls.py @@ -7,6 +7,7 @@ from common.api_helpers.optional_slash_router import optional_slash_path from .views import ( + AdaptiveGrafanaAlertingAPIView, AlertManagerAPIView, AmazonSNS, GrafanaAlertingAPIView, @@ -32,6 +33,11 @@ path("grafana_alerting//", GrafanaAlertingAPIView.as_view(), name="grafana_alerting"), path("alertmanager//", AlertManagerAPIView.as_view(), name="alertmanager"), path("amazon_sns//", AmazonSNS.as_view(), name="amazon_sns"), + path( + "adaptive_grafana_alerting//", + AdaptiveGrafanaAlertingAPIView.as_view(), + name="adaptive_grafana_alerting", + ), path("//", UniversalAPIView.as_view(), name="universal"), # integration backsync path("backsync/", IntegrationBacksyncAPIView.as_view(), name="integration_backsync"), diff --git a/engine/apps/integrations/views.py b/engine/apps/integrations/views.py index c51e8783c7..3dd27656bd 100644 --- a/engine/apps/integrations/views.py +++ b/engine/apps/integrations/views.py @@ -2,7 +2,9 @@ import logging from django.conf import settings +from django.core.cache import cache from django.core.exceptions import PermissionDenied +from django.db import transaction from django.http import HttpResponseBadRequest, JsonResponse from django.utils import timezone from django.utils.decorators import method_decorator @@ -12,7 +14,7 @@ from rest_framework.response import Response from rest_framework.views import APIView -from apps.alerts.models import AlertReceiveChannel +from apps.alerts.models import AlertReceiveChannel, ChannelFilter, EscalationChain from apps.auth_token.auth import IntegrationBacksyncAuthentication from apps.heartbeat.tasks import process_heartbeat_task from apps.integrations.legacy_prefix import has_legacy_prefix @@ -23,10 +25,14 @@ IntegrationRateLimitMixin, is_ratelimit_ignored, ) +from apps.integrations.mixins.alert_forwarding_mixin import AlertForwardingMixin from apps.integrations.tasks import create_alert, create_alertmanager_alerts from apps.integrations.throttlers.integration_backsync_throttler import BacksyncRateThrottle +from apps.slack.models import SlackChannel from apps.user_management.exceptions import OrganizationDeletedException, OrganizationMovedException +from apps.user_management.models import Organization, Team from common.api_helpers.utils import create_engine_url +from settings.base import SELF_HOSTED_SETTINGS logger = logging.getLogger(__name__) @@ -181,6 +187,7 @@ def check_integration_type(self, alert_receive_channel): return alert_receive_channel.integration in { AlertReceiveChannel.INTEGRATION_GRAFANA_ALERTING, AlertReceiveChannel.INTEGRATION_LEGACY_GRAFANA_ALERTING, + AlertReceiveChannel.INTEGRATION_ADAPTIVE_GRAFANA_ALERTING, } @@ -312,7 +319,9 @@ def check_integration_type(self, alert_receive_channel): return alert_receive_channel.integration == AlertReceiveChannel.INTEGRATION_GRAFANA -class UniversalAPIView(BrowsableInstructionMixin, AlertChannelDefiningMixin, IntegrationRateLimitMixin, APIView): +class UniversalAPIView( + BrowsableInstructionMixin, AlertChannelDefiningMixin, IntegrationRateLimitMixin, APIView, AlertForwardingMixin +): def post(self, request, *args, **kwargs): if request.FILES: # file-objects are not serializable when queuing the task @@ -342,6 +351,13 @@ def post(self, request, *args, **kwargs): ) return Response("Ok.") + def dispatch(self, *args, **kwargs): + forwarded = AlertForwardingMixin.dispatch(*args, **kwargs) + if forwarded: + return forwarded + + return AlertManagerAPIView.dispatch(self, *args, **kwargs) + class IntegrationHeartBeatAPIView(AlertChannelDefiningMixin, IntegrationHeartBeatRateLimitMixin, APIView): def get(self, request): @@ -369,3 +385,139 @@ def post(self, request): if integration_backsync_func: integration_backsync_func(alert_receive_channel, request.data) return Response(status=200) + + +class AdaptiveGrafanaAlertingAPIView(GrafanaAlertingAPIView): + def dispatch(self, *args, **kwargs): + token = str(kwargs["alert_channel_key"]) + """ + TODO: Will likely need service account token + grafana url to figure out organization + author, + Hard-coded for now + """ + organization = None + if settings.LICENSE != settings.OPEN_SOURCE_LICENSE_NAME: + instance_id = self.request.headers.get("X-Grafana-Org-Id") + if not instance_id: + return JsonResponse({"error": "Missing header X-Grafana-Org-Id"}, status=400) + organization = Organization.objects.filter(stack_id=instance_id).first() + else: + organization = Organization.objects.get( + stack_id=SELF_HOSTED_SETTINGS["STACK_ID"], org_id=SELF_HOSTED_SETTINGS["ORG_ID"] + ) + + if not organization: + return JsonResponse({"error": "Invalid oncall organization"}, status=400) + + routing_config, error = self.get_routing_config() + if error: + return JsonResponse({"error": error}, status=400) + + with transaction.atomic(): + receiver_name = routing_config.get("receiverName", None) + + team = None + team_name = routing_config.get("teamName", None) + if team_name: + try: + team = Team.objects.get(name=team_name) + except Team.DoesNotExist: + return JsonResponse({"error": "Invalid team name"}, status=400) + + alert_receive_channel, status = self.get_alert_receive_channel_from_short_term_cache(token) + if not alert_receive_channel: + alert_receive_channel, created = AlertReceiveChannel.objects.get_or_create( + verbal_name="Adaptive Grafana Alerting" if not receiver_name else receiver_name, + token=token, + organization=organization, + integration="adaptive_grafana_alerting", + team=team, + ) + if created: + cache_key = AlertChannelDefiningMixin.CACHE_KEY_SHORT_TERM + "_" + token + cache.delete(cache_key) + if receiver_name and receiver_name != alert_receive_channel.verbal_name: + alert_receive_channel.verbal_name = receiver_name + alert_receive_channel.save(update_fields=["verbal_name"]) + if team and team != alert_receive_channel.team: + alert_receive_channel.team = team + alert_receive_channel.save(update_fields=["team"]) + + escalation_chain = None + escalation_chain_id = routing_config.get("escalationChainId", None) + if escalation_chain_id: + try: + escalation_chain = EscalationChain.objects.get(public_primary_key=escalation_chain_id) + except EscalationChain.DoesNotExist: + return JsonResponse({"error": "Invalid escalation chain"}, status=400) + + # TODO: PoC Deal with other chatops MS teams and telegram later + slack_channel = None + slack_channel_id = routing_config.get("chatOps", {}).get("slackChannelId", None) + if slack_channel_id: + try: + slack_channel = SlackChannel.objects.get(slack_id=slack_channel_id) + except SlackChannel.DoesNotExist: + return JsonResponse({"error": "Invalid slack channel"}, status=400) + + if not escalation_chain and not slack_channel: + return JsonResponse( + {"error": "At least of 1 of escalationChainId or slackChannelId must be defined"}, status=400 + ) + + filtering_term = "" + if escalation_chain_id and slack_channel: + filtering_term = ( + f"{{{{ payload.get('routingConfig', {{}}).get('escalationChainId', None) == '{escalation_chain_id}' " + "and payload.get('routingConfig', {{}}).get('chatOps', {{}}).get('slackChannelId', None) == '{slack_channel_id}' }}}}" + ) + elif escalation_chain_id: + filtering_term = f"{{{{ payload.get('routingConfig', {{}}).get('escalationChainId', None) == '{escalation_chain_id}' }}}}" + elif slack_channel_id: + filtering_term = f"{{{{ payload.get('routingConfig', {{}}).get('chatOps', {{}}).get('slackChannelId', None) == '{slack_channel_id}' }}}}" + + _, filter_created = alert_receive_channel.channel_filters.get_or_create( + alert_receive_channel=alert_receive_channel, + filtering_term_type=ChannelFilter.FILTERING_TERM_TYPE_JINJA2, + escalation_chain=escalation_chain, + slack_channel=slack_channel, + filtering_term=filtering_term, + notify_in_slack=slack_channel is not None, + ) + + if filter_created: + self.update_channel_filter_order(alert_receive_channel) + + return AlertManagerAPIView.dispatch(self, *args, **kwargs) + + def get_routing_config(self): + try: + data = json.loads(self.request.body) + routing_config = data.get("routingConfig", None) + if not routing_config: + return None, "Missing routingConfig" + return routing_config, None + except json.JSONDecodeError: + return None, "Invalid JSON" + + def update_channel_filter_order(self, alert_receive_channel): + def categorize_filtering_term(term): + if not term: + return 3 + return ( + 0 + if "escalationChainId" in term and "slackChannelId" in term + else 1 + if "escalationChainId" in term + else 2 + if "slackChannelId" in term + else 3 + ) + + filters = list(alert_receive_channel.channel_filters.all().select_for_update()) + filters.sort(key=lambda obj: (categorize_filtering_term(obj.filtering_term), obj.id)) + for index, obj in enumerate(filters): + obj.order = 1000000 + index + ChannelFilter.objects.bulk_update(filters, ["order"]) + for index, obj in enumerate(filters): + obj.order = index + ChannelFilter.objects.bulk_update(filters, ["order"]) diff --git a/engine/config_integrations/adaptive_grafana_alerting.py b/engine/config_integrations/adaptive_grafana_alerting.py new file mode 100644 index 0000000000..6bc7bb9373 --- /dev/null +++ b/engine/config_integrations/adaptive_grafana_alerting.py @@ -0,0 +1,278 @@ +# Main +enabled = True +title = "Adaptive Grafana Alerting" +slug = "adaptive_grafana_alerting" +short_description = "Grafana Alerting integration, configured from alerting" +description = None +is_displayed_on_web = True +is_featured = False +is_able_to_autoresolve = True +is_demo_alert_enabled = False +based_on_alertmanager = True + + +# Behaviour +source_link = "{{ payload.alerts[0].generatorURL }}" + +grouping_id = "{{ payload.groupKey }}" + +resolve_condition = """{{ payload.status == "resolved" }}""" + +acknowledge_condition = None + +# Web +web_title = """\ +{% set groupLabels = payload.get("groupLabels", {}).copy() -%} +{% if "labels" in payload -%} +{# backward compatibility with legacy alertmanager integration -#} +{% set alertname = payload.get("labels", {}).get("alertname", "") -%} +{% else -%} +{% set alertname = groupLabels.pop("alertname", "") -%} +{% endif -%} + +{{ alertname }} {% if groupLabels | length > 0 %}({{ groupLabels.values()|join(", ") }}){% endif %} +""" # noqa + +web_message = """\ +{% set annotations = payload.get("commonAnnotations", {}).copy() -%} +{% set groupLabels = payload.get("groupLabels", {}) -%} +{% set commonLabels = payload.get("commonLabels", {}) -%} +{% set severity = groupLabels.severity -%} +{% set legacyLabels = payload.get("labels", {}) -%} +{% set legacyAnnotations = payload.get("annotations", {}) -%} + +{% if severity -%} +{% set severity_emoji = {"critical": ":rotating_light:", "warning": ":warning:" }[severity] | default(":question:") -%} +Severity: {{ severity }} {{ severity_emoji }} +{% endif -%} + +{% set status = payload.get("status", "Unknown") -%} +{% set status_emoji = {"firing": ":fire:", "resolved": ":white_check_mark:"}[status] | default(":warning:") -%} +Status: {{ status }} {{ status_emoji }} (on the source) +{% if status == "firing" and payload.numFiring -%} +Firing alerts – {{ payload.numFiring }} +Resolved alerts – {{ payload.numResolved }} +{% endif -%} + +{% if "runbook_url" in annotations -%} +[:book: Runbook:link:]({{ annotations.runbook_url }}) +{% set _ = annotations.pop('runbook_url') -%} +{% endif -%} + +{% if "runbook_url_internal" in annotations -%} +[:closed_book: Runbook (internal):link:]({{ annotations.runbook_url_internal }}) +{% set _ = annotations.pop('runbook_url_internal') -%} +{% endif %} + +{%- if groupLabels | length > 0 %} +GroupLabels: +{% for k, v in groupLabels.items() -%} +- {{ k }}: {{ v }} +{% endfor %} +{% endif -%} + +{% if commonLabels | length > 0 -%} +CommonLabels: +{% for k, v in commonLabels.items() -%} +- {{ k }}: {{ v }} +{% endfor %} +{% endif -%} + +{% if annotations | length > 0 -%} +Annotations: +{% for k, v in annotations.items() -%} +- {{ k }}: {{ v }} +{% endfor %} +{% endif -%} + +{# backward compatibility with legacy alertmanager integration -#} +{% if legacyLabels | length > 0 -%} +Labels: +{% for k, v in legacyLabels.items() -%} +- {{ k }}: {{ v }} +{% endfor %} +{% endif -%} + +{% if legacyAnnotations | length > 0 -%} +Annotations: +{% for k, v in legacyAnnotations.items() -%} +- {{ k }}: {{ v }} +{% endfor %} +{% endif -%} +[View in AlertManager]({{ source_link }}) +""" + + +# Slack +slack_title = """\ +*<{{ grafana_oncall_link }}|#{{ grafana_oncall_incident_id }} {{ web_title }}>* via {{ integration_name }} +{% if source_link and (source_link[:8] == "https://" or source_link[:7] == "http://") %} + (*<{{ source_link }}|source>*) +{%- endif %} +""" + +# default slack message template is identical to web message template, except urls +# It can be based on web message template (see example), but it can affect existing templates +# slack_message = """ +# {% set mkdwn_link_regex = "\[([\w\s\d:]+)\]\((https?:\/\/[\w\d./?=#]+)\)" %} +# {{ web_message +# | regex_replace(mkdwn_link_regex, "<\\2|\\1>") +# }} +# """ + +slack_message = """\ +{% set annotations = payload.get("commonAnnotations", {}).copy() -%} +{% set groupLabels = payload.get("groupLabels", {}) -%} +{% set commonLabels = payload.get("commonLabels", {}) -%} +{% set severity = groupLabels.severity -%} +{% set legacyLabels = payload.get("labels", {}) -%} +{% set legacyAnnotations = payload.get("annotations", {}) -%} + +{% if severity -%} +{% set severity_emoji = {"critical": ":rotating_light:", "warning": ":warning:" }[severity] | default(":question:") -%} +Severity: {{ severity }} {{ severity_emoji }} +{% endif -%} + +{% set status = payload.get("status", "Unknown") -%} +{% set status_emoji = {"firing": ":fire:", "resolved": ":white_check_mark:"}[status] | default(":warning:") -%} +Status: {{ status }} {{ status_emoji }} (on the source) +{% if status == "firing" and payload.numFiring -%} +Firing alerts – {{ payload.numFiring }} +Resolved alerts – {{ payload.numResolved }} +{% endif -%} + +{% if "runbook_url" in annotations -%} +<{{ annotations.runbook_url }}|:book: Runbook:link:> +{% set _ = annotations.pop('runbook_url') -%} +{% endif -%} + +{% if "runbook_url_internal" in annotations -%} +<{{ annotations.runbook_url_internal }}|:closed_book: Runbook (internal):link:> +{% set _ = annotations.pop('runbook_url_internal') -%} +{% endif %} + +{%- if groupLabels | length > 0 %} +GroupLabels: +{% for k, v in groupLabels.items() -%} +- {{ k }}: {{ v }} +{% endfor %} +{% endif -%} + +{% if commonLabels | length > 0 -%} +CommonLabels: +{% for k, v in commonLabels.items() -%} +- {{ k }}: {{ v }} +{% endfor %} +{% endif -%} + +{% if annotations | length > 0 -%} +Annotations: +{% for k, v in annotations.items() -%} +- {{ k }}: {{ v }} +{% endfor %} +{% endif -%} + +{# backward compatibility with legacy alertmanager integration -#} +{% if legacyLabels | length > 0 -%} +Labels: +{% for k, v in legacyLabels.items() -%} +- {{ k }}: {{ v }} +{% endfor %} +{% endif -%} + +{% if legacyAnnotations | length > 0 -%} +Annotations: +{% for k, v in legacyAnnotations.items() -%} +- {{ k }}: {{ v }} +{% endfor %} +{% endif -%} +""" +# noqa: W291 + + +slack_image_url = None + +web_image_url = None + +# SMS +sms_title = web_title + +# Phone +phone_call_title = """{{ payload.get("groupLabels", {}).values() |join(", ") }}""" + +# Telegram +telegram_title = web_title + +telegram_message = """\ +{% set annotations = payload.get("commonAnnotations", {}).copy() -%} +{% set groupLabels = payload.get("groupLabels", {}) -%} +{% set commonLabels = payload.get("commonLabels", {}) -%} +{% set severity = groupLabels.severity -%} +{% set legacyLabels = payload.get("labels", {}) -%} +{% set legacyAnnotations = payload.get("annotations", {}) -%} + +{% if severity -%} +{% set severity_emoji = {"critical": ":rotating_light:", "warning": ":warning:" }[severity] | default(":question:") -%} +Severity: {{ severity }} {{ severity_emoji }} +{% endif -%} + +{% set status = payload.get("status", "Unknown") -%} +{% set status_emoji = {"firing": ":fire:", "resolved": ":white_check_mark:"}[status] | default(":warning:") -%} +Status: {{ status }} {{ status_emoji }} (on the source) +{% if status == "firing" and payload.numFiring -%} +Firing alerts – {{ payload.numFiring }} +Resolved alerts – {{ payload.numResolved }} +{% endif -%} + +{% if "runbook_url" in annotations -%} +:book: Runbook:link: +{% set _ = annotations.pop('runbook_url') -%} +{% endif -%} + +{% if "runbook_url_internal" in annotations -%} +:closed_book: Runbook (internal):link: +{% set _ = annotations.pop('runbook_url_internal') -%} +{% endif %} + +{%- if groupLabels | length > 0 %} +GroupLabels: +{% for k, v in groupLabels.items() -%} +- {{ k }}: {{ v }} +{% endfor %} +{% endif -%} + +{% if commonLabels | length > 0 -%} +CommonLabels: +{% for k, v in commonLabels.items() -%} +- {{ k }}: {{ v }} +{% endfor %} +{% endif -%} + +{% if annotations | length > 0 -%} +Annotations: +{% for k, v in annotations.items() -%} +- {{ k }}: {{ v }} +{% endfor %} +{% endif -%} + +{# backward compatibility with legacy alertmanager integration -#} +{% if legacyLabels | length > 0 -%} +Labels: +{% for k, v in legacyLabels.items() -%} +- {{ k }}: {{ v }} +{% endfor %} +{% endif -%} + +{% if legacyAnnotations | length > 0 -%} +Annotations: +{% for k, v in legacyAnnotations.items() -%} +- {{ k }}: {{ v }} +{% endfor %} +{% endif -%} +View in AlertManager +""" + +telegram_image_url = None + + +example_payload = None diff --git a/engine/settings/base.py b/engine/settings/base.py index 0f73c8d5af..dedd74e424 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -890,6 +890,7 @@ class BrokerTypes: # Legacy are not shown, ordering isn't important "config_integrations.legacy_alertmanager", "config_integrations.legacy_grafana_alerting", + "config_integrations.adaptive_grafana_alerting", ] ADVANCED_WEBHOOK_PRESET = "apps.webhooks.presets.advanced.AdvancedWebhookPreset"