commit
444f401d67
19 changed files with 105 additions and 61 deletions
|
|
@ -1,5 +1,9 @@
|
|||
# Change Log
|
||||
|
||||
## v1.0.26 (2022-08-26)
|
||||
- Insight log's format fixes
|
||||
- Remove UserNotificationPolicy auto-recreating
|
||||
|
||||
## v1.0.25 (2022-08-24)
|
||||
- Bug fixes
|
||||
|
||||
|
|
|
|||
|
|
@ -16,15 +16,22 @@ weight: 300
|
|||
|
||||
# Telegram integration for Grafana OnCall
|
||||
|
||||
You can use Telegram to deliver alert group notifications to a dedicated channel, and allow users to perform notification actions.
|
||||
You can manage alerts either directly in your personal Telegram DMs or in a dedicated team channel.
|
||||
|
||||
Each alert group notification is assigned a dedicated discussion. Users can perform notification actions (acknowledge, resolve, silence), create reports, and discuss alerts in the comments section of the discussions.
|
||||
## Configure Telegram user settings in Grafana OnCall
|
||||
|
||||
In case an integration route is not configured to use a Telegram channel, users will receive messages with alert group contents, logs and actions in their DMs.
|
||||
To receive alert group contents, escalation logs and to be able to perform actions (acknowledge, resolve, silence) in Telegram DMs, please refer to the following steps:
|
||||
|
||||
## Connect to Telegram
|
||||
1. In your profile, find the Telegram setting and click **Connect**.
|
||||
1. Click **Connect automatically** for the bot to message you and to bring up your telegram account.
|
||||
1. Click **Start** when the OnCall bot messages you and wait for the connection confirmation.
|
||||
1. Done! Now you can receive alerts directly to your Telegram DMs.
|
||||
|
||||
Connect your organization's Telegram account to your Grafana OnCall instance by following the instructions provided in OnCall. You can use the following steps as a reference.
|
||||
If you want to connect manually, you can click the URL provided and then **SEND MESSAGE**. In your Telegram account, click **Start**.
|
||||
|
||||
## (Optional) Connect to a Telegram channel
|
||||
|
||||
In case you want to manage alerts in a dedicated Telegram channel, please use the following steps as a reference.
|
||||
|
||||
> **NOTE:** Only Grafana users with the administrator role can configure OnCall settings.
|
||||
|
||||
|
|
@ -42,10 +49,5 @@ Connect your organization's Telegram account to your Grafana OnCall instance by
|
|||
1. In OnCall, send the provided verification code to the channel.
|
||||
1. Make sure users connect to Telegram in their OnCall user profile.
|
||||
|
||||
## Configure Telegram user settings in OnCall
|
||||
|
||||
1. In your profile, find the Telegram setting and click **Connect**.
|
||||
1. Click **Connect automatically** for the bot to message you and to bring up your telegram account.
|
||||
1. Click **Start** when the OnCall bot messages you.
|
||||
|
||||
If you want to connect manually, you can click the URL provided and then **SEND MESSAGE**. In your Telegram account, click **Start**.
|
||||
Each alert group is assigned a dedicated discussion. Users can perform actions (acknowledge, resolve, silence), and discuss alerts in the comments section of the discussions.
|
||||
In case an integration route is not configured to use a Telegram channel, users will receive messages with alert group contents, logs and actions in their DMs.
|
||||
|
|
|
|||
|
|
@ -166,13 +166,11 @@ lt --port 8080 -s pretty-turkey-83 --print-requests
|
|||
|
||||
The Telegram integration for Grafana OnCall is designed for collaborative team work and improved incident response. Refer to the following steps to configure the Telegram integration:
|
||||
|
||||
1. Ensure your OnCall environment is up and running.
|
||||
|
||||
1. Request [BotFather](https://t.me/BotFather) for a key, then add your key in `TELEGRAM_TOKEN` in your Grafana OnCall **Env Variables**.
|
||||
|
||||
1. Set `TELEGRAM_WEBHOOK_HOST` with your external URL for your Grafana OnCall.
|
||||
|
||||
1. From the **ChatOps** tab in Grafana OnCall, click **Telegram**.
|
||||
1. Ensure your Grafana OnCall environment is up and running.
|
||||
2. Create a Telegram bot using [BotFather](https://t.me/BotFather) and save the token provided by BotFather. Please make sure to disable **Group Privacy** for the bot (Bot Settings -> Group Privacy -> Turn off).
|
||||
3. Paste the token provided by BotFather to the `TELEGRAM_TOKEN` variable on the **Env Variables** page of your Grafana OnCall instance.
|
||||
4. Set the `TELEGRAM_WEBHOOK_HOST` variable to the external address of your Grafana OnCall instance. Please note that `TELEGRAM_WEBHOOK_HOST` must start with `https://` and be publicly available (meaning that it can be reached by Telegram servers). If your host is private or local, consider using a reverse proxy (e.g. [ngrok](https://ngrok.com)).
|
||||
5. Now you can connect Telegram accounts on the **Users** page and receive alert groups to Telegram direct messages. Alternatively, in case you want to connect Telegram channels to your Grafana OnCall environment, navigate to the **ChatOps** tab.
|
||||
|
||||
## Grafana OSS-Cloud Setup
|
||||
|
||||
|
|
|
|||
|
|
@ -659,9 +659,7 @@ class IncidentLogBuilder:
|
|||
# last passed step order + 1
|
||||
notification_policy_order = last_user_log.notification_policy.order + 1
|
||||
|
||||
notification_policies = UserNotificationPolicy.objects.get_or_create_for_user(
|
||||
user=user_to_notify, important=important
|
||||
)
|
||||
notification_policies = UserNotificationPolicy.objects.filter(user=user_to_notify, important=important)
|
||||
|
||||
for notification_policy in notification_policies:
|
||||
future_notification = notification_policy.order >= notification_policy_order
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ def notify_group_task(alert_group_pk, escalation_policy_snapshot_order=None):
|
|||
if not user.is_notification_allowed:
|
||||
continue
|
||||
|
||||
notification_policies = UserNotificationPolicy.objects.get_or_create_for_user(
|
||||
notification_policies = UserNotificationPolicy.objects.filter(
|
||||
user=user,
|
||||
important=escalation_policy_step == EscalationPolicy.STEP_NOTIFY_GROUP_IMPORTANT,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -73,9 +73,12 @@ def notify_user_task(
|
|||
user_has_notification = UserHasNotification.objects.filter(pk=user_has_notification.pk).select_for_update()[0]
|
||||
|
||||
if previous_notification_policy_pk is None:
|
||||
notification_policy = UserNotificationPolicy.objects.get_or_create_for_user(
|
||||
user=user, important=important
|
||||
).first()
|
||||
notification_policy = UserNotificationPolicy.objects.filter(user=user, important=important).first()
|
||||
if notification_policy is None:
|
||||
task_logger.info(
|
||||
f"notify_user_task: Failed to notify. No notification policies. user_id={user_pk} alert_group_id={alert_group_pk} important={important}"
|
||||
)
|
||||
return
|
||||
# Here we collect a brief overview of notification steps configured for user to send it to thread.
|
||||
collected_steps_ids = []
|
||||
next_notification_policy = notification_policy.next()
|
||||
|
|
|
|||
|
|
@ -361,8 +361,8 @@ class UserView(
|
|||
author=request.user,
|
||||
event_name=ChatOpsEvent.USER_UNLINKED,
|
||||
chatops_type=ChatOpsType.TELEGRAM,
|
||||
user=user.username,
|
||||
user_id=user.public_primary_key,
|
||||
linked_user=user.username,
|
||||
linked_user_id=user.public_primary_key,
|
||||
)
|
||||
except TelegramToUserConnector.DoesNotExist:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
|
@ -383,8 +383,8 @@ class UserView(
|
|||
author=request.user,
|
||||
event_name=ChatOpsEvent.USER_UNLINKED,
|
||||
chatops_type=backend.backend_id,
|
||||
user=user.username,
|
||||
user_id=user.public_primary_key,
|
||||
linked_user=user.username,
|
||||
linked_user_id=user.public_primary_key,
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from apps.base.models.user_notification_policy import BUILT_IN_BACKENDS, Notific
|
|||
from apps.user_management.models import User
|
||||
from common.api_helpers.exceptions import BadRequest
|
||||
from common.api_helpers.mixins import UpdateSerializerMixin
|
||||
from common.exceptions import UserNotificationPolicyCouldNotBeDeleted
|
||||
from common.insight_log import EntityEvent, write_resource_insight_log
|
||||
|
||||
|
||||
|
|
@ -55,14 +56,14 @@ class UserNotificationPolicyView(UpdateSerializerMixin, ModelViewSet):
|
|||
except ValueError:
|
||||
raise BadRequest(detail="Invalid user param")
|
||||
if user_id is None or user_id == self.request.user.public_primary_key:
|
||||
queryset = self.model.objects.get_or_create_for_user(user=self.request.user, important=important)
|
||||
queryset = self.model.objects.filter(user=self.request.user, important=important)
|
||||
else:
|
||||
try:
|
||||
target_user = User.objects.get(public_primary_key=user_id)
|
||||
except User.DoesNotExist:
|
||||
raise BadRequest(detail="User does not exist")
|
||||
|
||||
queryset = self.model.objects.get_or_create_for_user(user=target_user, important=important)
|
||||
queryset = self.model.objects.filter(user=target_user, important=important)
|
||||
|
||||
queryset = self.serializer_class.setup_eager_loading(queryset)
|
||||
|
||||
|
|
@ -111,7 +112,10 @@ class UserNotificationPolicyView(UpdateSerializerMixin, ModelViewSet):
|
|||
def perform_destroy(self, instance):
|
||||
user = instance.user
|
||||
prev_state = user.insight_logs_serialized
|
||||
instance.delete()
|
||||
try:
|
||||
instance.delete()
|
||||
except UserNotificationPolicyCouldNotBeDeleted:
|
||||
raise BadRequest(detail="Can't delete last user notification policy")
|
||||
new_state = user.insight_logs_serialized
|
||||
write_resource_insight_log(
|
||||
instance=user,
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ from typing import Tuple
|
|||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import models, transaction
|
||||
from django.db import models
|
||||
from django.db.models import Q, QuerySet
|
||||
from django.utils import timezone
|
||||
from ordered_model.models import OrderedModel
|
||||
|
||||
from apps.base.messaging import get_messaging_backends
|
||||
from apps.user_management.models import User
|
||||
from common.exceptions import UserNotificationPolicyCouldNotBeDeleted
|
||||
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length
|
||||
|
||||
|
||||
|
|
@ -69,24 +70,6 @@ def validate_channel_choice(value):
|
|||
|
||||
|
||||
class UserNotificationPolicyQuerySet(models.QuerySet):
|
||||
def get_or_create_for_user(self, user: User, important: bool) -> "QuerySet[UserNotificationPolicy]":
|
||||
with transaction.atomic():
|
||||
User.objects.select_for_update().get(pk=user.pk)
|
||||
return self._get_or_create_for_user(user, important)
|
||||
|
||||
def _get_or_create_for_user(self, user: User, important: bool) -> "QuerySet[UserNotificationPolicy]":
|
||||
notification_policies = super().filter(user=user, important=important)
|
||||
|
||||
if notification_policies.exists():
|
||||
return notification_policies
|
||||
|
||||
if important:
|
||||
policies = self.create_important_policies_for_user(user)
|
||||
else:
|
||||
policies = self.create_default_policies_for_user(user)
|
||||
|
||||
return policies
|
||||
|
||||
def create_default_policies_for_user(self, user: User) -> "QuerySet[UserNotificationPolicy]":
|
||||
model = self.model
|
||||
|
||||
|
|
@ -197,6 +180,12 @@ class UserNotificationPolicy(OrderedModel):
|
|||
else:
|
||||
return "Not set"
|
||||
|
||||
def delete(self):
|
||||
if UserNotificationPolicy.objects.filter(important=self.important, user=self.user).count() == 1:
|
||||
raise UserNotificationPolicyCouldNotBeDeleted("Can't delete last user notification policy")
|
||||
else:
|
||||
super().delete()
|
||||
|
||||
|
||||
class NotificationChannelOptions:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from apps.base.models.user_notification_policy import (
|
|||
validate_channel_choice,
|
||||
)
|
||||
from apps.base.tests.messaging_backend import TestOnlyBackend
|
||||
from common.exceptions import UserNotificationPolicyCouldNotBeDeleted
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -80,3 +81,25 @@ def test_extra_messaging_backends_details():
|
|||
)
|
||||
|
||||
assert validate_channel_choice(channel_choice) is None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_unable_to_delete_last_notification_policy(
|
||||
make_organization,
|
||||
make_user_for_organization,
|
||||
make_user_notification_policy,
|
||||
):
|
||||
organization = make_organization()
|
||||
user = make_user_for_organization(organization)
|
||||
|
||||
first_policy = make_user_notification_policy(
|
||||
user, UserNotificationPolicy.Step.NOTIFY, notify_by=UserNotificationPolicy.NotificationChannel.SLACK
|
||||
)
|
||||
|
||||
second_policy = make_user_notification_policy(
|
||||
user, UserNotificationPolicy.Step.WAIT, wait_delay=timedelta(minutes=5)
|
||||
)
|
||||
|
||||
first_policy.delete()
|
||||
with pytest.raises(UserNotificationPolicyCouldNotBeDeleted):
|
||||
second_policy.delete()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from apps.user_management.models import User
|
|||
from common.api_helpers.exceptions import BadRequest
|
||||
from common.api_helpers.mixins import RateLimitHeadersMixin, UpdateSerializerMixin
|
||||
from common.api_helpers.paginators import FiftyPageSizePaginator
|
||||
from common.exceptions import UserNotificationPolicyCouldNotBeDeleted
|
||||
from common.insight_log import EntityEvent, write_resource_insight_log
|
||||
|
||||
|
||||
|
|
@ -74,7 +75,10 @@ class PersonalNotificationView(RateLimitHeadersMixin, UpdateSerializerMixin, Mod
|
|||
def perform_destroy(self, instance):
|
||||
user = self.request.user
|
||||
prev_state = user.insight_logs_serialized
|
||||
instance.delete()
|
||||
try:
|
||||
instance.delete()
|
||||
except UserNotificationPolicyCouldNotBeDeleted:
|
||||
raise BadRequest(detail="Can't delete last user notification policy")
|
||||
new_state = user.insight_logs_serialized
|
||||
write_resource_insight_log(
|
||||
instance=user,
|
||||
|
|
|
|||
|
|
@ -72,8 +72,8 @@ def connect_user_to_slack(response, backend, strategy, user, organization, *args
|
|||
author=user,
|
||||
event_name=ChatOpsEvent.USER_LINKED,
|
||||
chatops_type=ChatOpsType.SLACK,
|
||||
user=user.username,
|
||||
user_id=user.public_primary_key,
|
||||
linked_user=user.username,
|
||||
linked_user_id=user.public_primary_key,
|
||||
)
|
||||
user.slack_user_identity = slack_user_identity
|
||||
user.save(update_fields=["slack_user_identity"])
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ class TelegramVerificationCode(models.Model):
|
|||
author=user,
|
||||
event_name=ChatOpsEvent.USER_LINKED,
|
||||
chatops_type=ChatOpsType.TELEGRAM,
|
||||
user=user.username,
|
||||
user_id=user.public_primary_key,
|
||||
linked_user=user.username,
|
||||
linked_user_id=user.public_primary_key,
|
||||
)
|
||||
return connector, created
|
||||
|
||||
|
|
|
|||
|
|
@ -260,6 +260,9 @@ class User(models.Model):
|
|||
# TODO: check whether this signal can be moved to save method of the model
|
||||
@receiver(post_save, sender=User)
|
||||
def listen_for_user_model_save(sender, instance, created, *args, **kwargs):
|
||||
if created:
|
||||
instance.notification_policies.create_default_policies_for_user(instance)
|
||||
instance.notification_policies.create_important_policies_for_user(instance)
|
||||
drop_cached_ical_for_custom_events_for_organization.apply_async(
|
||||
(instance.organization_id,),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1 +1,6 @@
|
|||
from .exceptions import MaintenanceCouldNotBeStartedError, TeamCanNotBeChangedError, UnableToSendDemoAlert # noqa: F401
|
||||
from .exceptions import ( # noqa: F401
|
||||
MaintenanceCouldNotBeStartedError,
|
||||
TeamCanNotBeChangedError,
|
||||
UnableToSendDemoAlert,
|
||||
UserNotificationPolicyCouldNotBeDeleted,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,3 +17,7 @@ class TeamCanNotBeChangedError(OperationCouldNotBePerformedError):
|
|||
|
||||
class UnableToSendDemoAlert(OperationCouldNotBePerformedError):
|
||||
pass
|
||||
|
||||
|
||||
class UserNotificationPolicyCouldNotBeDeleted(OperationCouldNotBePerformedError):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ def write_chatops_insight_log(author, event_name: ChatOpsEvent, chatops_type: Ch
|
|||
user_id = author.public_primary_key
|
||||
username = json.dumps(author.username)
|
||||
|
||||
log_line = f'tenant_id={tenant_id} author_id={user_id} author={username} action_type="chat_ops" action_name={event_name.value} chat_ops_type={chatops_type.value}' # noqa
|
||||
log_line = f"tenant_id={tenant_id} author_id={user_id} author={username} action_type=chat_ops action_name={event_name.value} chat_ops_type={chatops_type.value}" # noqa
|
||||
for k, v in kwargs.items():
|
||||
log_line += f" {k}={json.dumps(v)}"
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ from apps.telegram.tests.factories import (
|
|||
TelegramVerificationCodeFactory,
|
||||
)
|
||||
from apps.twilioapp.tests.factories import PhoneCallFactory, SMSFactory
|
||||
from apps.user_management.models.user import User, listen_for_user_model_save
|
||||
from apps.user_management.tests.factories import OrganizationFactory, TeamFactory, UserFactory
|
||||
from common.constants.role import Role
|
||||
|
||||
|
|
@ -150,7 +151,9 @@ def make_organization():
|
|||
@pytest.fixture
|
||||
def make_user_for_organization():
|
||||
def _make_user_for_organization(organization, role=Role.ADMIN, **kwargs):
|
||||
post_save.disconnect(listen_for_user_model_save, sender=User)
|
||||
user = UserFactory(organization=organization, role=role, **kwargs)
|
||||
post_save.disconnect(listen_for_user_model_save, sender=User)
|
||||
return user
|
||||
|
||||
return _make_user_for_organization
|
||||
|
|
|
|||
|
|
@ -147,12 +147,16 @@ const TelegramModal = (props: TelegramModalProps) => {
|
|||
<Icon name="copy" />
|
||||
</CopyToClipboard>
|
||||
</span>{' '}
|
||||
, to the channel.
|
||||
, to the channel and wait for the confirmation message.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className={cx('telegram-instruction-container')}>
|
||||
<Text>8. Make sure users connect to Telegram in their OnCall user profile.</Text>
|
||||
<Text>8. Make sure users connect their Telegram accounts in their OnCall user profile.</Text>
|
||||
</div>
|
||||
|
||||
<div className={cx('telegram-instruction-container')}>
|
||||
<Text>9. Done! Now you can manage alerts in your Telegram workspace.</Text>
|
||||
</div>
|
||||
|
||||
<div className={cx('telegram-instruction-container')}>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue