From 4765c9b07c8ee4e3cd6930dfa629df4416572dd1 Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Wed, 24 Aug 2022 12:04:44 +0500 Subject: [PATCH] Insight logs (#348) * Entity events insight logs * Insight logging * Fix event for updating templates * Format fixes * Remove organization_log_type.py * Simplify signature of chatops_insight_log * insight logs formatting * Add possibility to enable all insight logging via DynamicSetting * Fixes * Style fixes * Add migration * Fix migration --- .../alerts/models/alert_receive_channel.py | 136 +++----- engine/apps/alerts/models/channel_filter.py | 78 +++-- engine/apps/alerts/models/custom_button.py | 51 ++- engine/apps/alerts/models/escalation_chain.py | 36 +- .../apps/alerts/models/escalation_policy.py | 95 +++--- .../apps/alerts/models/maintainable_object.py | 10 +- engine/apps/alerts/tasks/maintenance.py | 25 +- .../apps/alerts/tests/test_channel_filter.py | 23 -- engine/apps/alerts/tests/test_maintenance.py | 10 +- engine/apps/api/serializers/organization.py | 4 +- .../serializers/organization_log_record.py | 38 --- .../api/tests/test_organization_log_record.py | 242 ------------- engine/apps/api/urls.py | 2 - .../apps/api/views/alert_receive_channel.py | 25 +- .../views/alert_receive_channel_template.py | 23 +- engine/apps/api/views/channel_filter.py | 69 ++-- engine/apps/api/views/custom_button.py | 36 +- engine/apps/api/views/escalation_chain.py | 51 ++- engine/apps/api/views/escalation_policy.py | 67 ++-- .../apps/api/views/integration_heartbeat.py | 33 +- engine/apps/api/views/on_call_shifts.py | 37 +- engine/apps/api/views/organization.py | 15 +- .../apps/api/views/organization_log_record.py | 128 ------- engine/apps/api/views/public_api_tokens.py | 8 +- engine/apps/api/views/schedule.py | 48 ++- engine/apps/api/views/slack_team_settings.py | 15 +- engine/apps/api/views/telegram_channels.py | 12 +- engine/apps/api/views/user.py | 81 +++-- .../api/views/user_notification_policy.py | 53 ++- .../apps/auth_token/models/api_auth_token.py | 21 +- .../models/schedule_export_auth_token.py | 23 +- .../models/user_schedule_export_auth_token.py | 21 +- .../base/migrations/0001_squashed_initial.py | 3 +- .../0003_delete_organizationlogrecord.py | 16 + engine/apps/base/models/__init__.py | 1 - .../base/models/organization_log_record.py | 317 ------------------ .../base/models/user_notification_policy.py | 10 - engine/apps/base/tests/factories.py | 9 +- .../tests/test_organization_log_record.py | 18 - engine/apps/heartbeat/models.py | 31 +- engine/apps/public_api/views/action.py | 36 +- .../public_api/views/escalation_chains.py | 45 +-- .../public_api/views/escalation_policies.py | 42 +-- engine/apps/public_api/views/integrations.py | 23 +- .../apps/public_api/views/on_call_shifts.py | 36 +- .../views/personal_notifications.py | 55 ++- engine/apps/public_api/views/routes.py | 47 +-- engine/apps/public_api/views/schedules.py | 35 +- .../schedules/models/custom_on_call_shift.py | 62 ++++ .../apps/schedules/models/on_call_schedule.py | 110 +++--- .../apps/slack/models/slack_team_identity.py | 7 +- .../slack/scenarios/alertgroup_appearance.py | 17 +- engine/apps/slack/scenarios/schedules.py | 18 +- engine/apps/slack/views.py | 9 +- engine/apps/social_auth/pipeline.py | 9 + .../telegram/models/connectors/channel.py | 19 +- .../telegram/models/verification/channel.py | 26 +- .../telegram/models/verification/personal.py | 15 +- .../user_management/models/organization.py | 53 +-- engine/apps/user_management/models/user.py | 56 ++-- .../organization_log_creator/__init__.py | 2 - .../create_organization_log.py | 11 - .../organization_log_type.py | 52 --- .../tests/test_organization.py | 4 - engine/common/insight_log/__init__.py | 3 + .../insight_log/chatops_insight_logs.py | 45 +++ .../insight_log/insight_logs_enabled_check.py | 15 + .../insight_log/maintenance_insight_log.py | 38 +++ .../insight_log/resource_insight_logs.py | 126 +++++++ engine/common/public_primary_keys.py | 1 - engine/conftest.py | 13 - engine/settings/base.py | 2 +- 72 files changed, 1225 insertions(+), 1728 deletions(-) delete mode 100644 engine/apps/api/serializers/organization_log_record.py delete mode 100644 engine/apps/api/tests/test_organization_log_record.py delete mode 100644 engine/apps/api/views/organization_log_record.py create mode 100644 engine/apps/base/migrations/0003_delete_organizationlogrecord.py delete mode 100644 engine/apps/base/models/organization_log_record.py delete mode 100644 engine/apps/base/tests/test_organization_log_record.py delete mode 100644 engine/apps/user_management/organization_log_creator/__init__.py delete mode 100644 engine/apps/user_management/organization_log_creator/create_organization_log.py delete mode 100644 engine/apps/user_management/organization_log_creator/organization_log_type.py create mode 100644 engine/common/insight_log/__init__.py create mode 100644 engine/common/insight_log/chatops_insight_logs.py create mode 100644 engine/common/insight_log/insight_logs_enabled_check.py create mode 100644 engine/common/insight_log/maintenance_insight_log.py create mode 100644 engine/common/insight_log/resource_insight_logs.py diff --git a/engine/apps/alerts/models/alert_receive_channel.py b/engine/apps/alerts/models/alert_receive_channel.py index 643f737e..60704773 100644 --- a/engine/apps/alerts/models/alert_receive_channel.py +++ b/engine/apps/alerts/models/alert_receive_channel.py @@ -27,9 +27,9 @@ from apps.integrations.tasks import create_alert, create_alertmanager_alerts from apps.slack.constants import SLACK_RATE_LIMIT_DELAY, SLACK_RATE_LIMIT_TIMEOUT from apps.slack.tasks import post_slack_rate_limit_message from apps.slack.utils import post_message_to_channel -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.utils import create_engine_url from common.exceptions import TeamCanNotBeChangedError, UnableToSendDemoAlert +from common.insight_log import EntityEvent, write_resource_insight_log from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length logger = logging.getLogger(__name__) @@ -342,66 +342,6 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject): self.save(update_fields=["rate_limit_message_task_id", "rate_limited_in_slack_at"]) post_slack_rate_limit_message.apply_async((self.pk,), countdown=delay, task_id=task_id) - @property - def repr_settings_for_client_side_logging(self): - """ - Example of execution: - name: Grafana :blush:, team: example, auto resolve allowed: Yes - templates: - Slack title: *<{{ grafana_oncall_link }}|#{{ grafana_oncall_id }} Custom title>* via {{ integration_name }} - {% if source_link %} - (*<{{ source_link }}|source>*) - {%- endif %}, - Slack message: default, - Slack image url: default, - SMS title: default, - Phone call title: default, - Web title: default, - Web message: default, - Web image url: default, - Email title: default, - Email message: default, - Telegram title: default, - Telegram message: default, - Telegram image url: default, - Source link: default, - Grouping id: default, - Resolve condition: default, - Acknowledge condition: default - """ - result = f"name: {self.verbal_name}, team: {self.team.name if self.team else 'No team'}" - if self.is_able_to_autoresolve: - result += f", auto resolve allowed: {'Yes' if self.allow_source_based_resolving else 'No'}" - if self.integration == AlertReceiveChannel.INTEGRATION_SLACK_CHANNEL: - slack_channel = None - if self.integration_slack_channel_id: - SlackChannel = apps.get_model("slack", "SlackChannel") - slack_channel = SlackChannel.objects.filter( - slack_team_identity=self.organization.slack_team_identity, - slack_id=self.integration_slack_channel_id, - ).first() - result += f", slack channel: {slack_channel.name if slack_channel else 'not selected'}" - result += ( - f"\ntemplates:\nSlack title: {self.slack_title_template or 'default'},\n" - f"Slack message: {self.slack_message_template or 'default'},\n" - f"Slack image url: {self.slack_image_url_template or 'default'},\n" - f"SMS title: {self.sms_title_template or 'default'},\n" - f"Phone call title: {self.phone_call_title_template or 'default'},\n" - f"Web title: {self.web_title_template or 'default'},\n" - f"Web message: {self.web_message_template or 'default'},\n" - f"Web image url: {self.web_image_url_template or 'default'},\n" - f"Email title: {self.email_title_template or 'default'},\n" - f"Email message: {self.email_message_template or 'default'},\n" - f"Telegram title: {self.telegram_title_template or 'default'},\n" - f"Telegram message: {self.telegram_message_template or 'default'},\n" - f"Telegram image url: {self.telegram_image_url_template or 'default'},\n" - f"Source link: {self.source_link_template or 'default'},\n" - f"Grouping id: {self.grouping_id_template or 'default'},\n" - f"Resolve condition: {self.resolve_condition_template or 'default'},\n" - f"Acknowledge condition: {self.acknowledge_condition_template or 'default'}" - ) - return result - @property def alert_groups_count(self): return self.alert_groups.count() @@ -658,6 +598,55 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject): AlertReceiveChannel.INTEGRATION_GRAFANA_ALERTING, ) + # Insight logs + @property + def insight_logs_type_verbal(self): + return "integration" + + @property + def insight_logs_verbal(self): + return self.verbal_name + + @property + def insight_logs_serialized(self): + result = { + "name": self.verbal_name, + "allow_source_based_resolving": self.allow_source_based_resolving, + "slack_title": self.slack_title_template or "default", + "slack_message": self.slack_message_template or "default", + "slack_image_url": self.slack_image_url_template or "default", + "sms_title": self.sms_title_template or "default", + "phone_call_title": self.phone_call_title_template or "default", + "web_title": self.web_title_template or "default", + "web_message": self.web_message_template or "default", + "web_image_url_template": self.web_image_url_template or "default", + "email_title_template": self.email_title_template or "default", + "email_message": self.email_message_template or "default", + "telegram_title": self.telegram_title_template or "default", + "telegram_message": self.telegram_message_template or "default", + "telegram_image_url": self.telegram_image_url_template or "default", + "source_link": self.source_link_template or "default", + "grouping_id": self.grouping_id_template or "default", + "resolve_condition": self.resolve_condition_template or "default", + "acknowledge_condition": self.acknowledge_condition_template or "default", + } + if self.team: + result["team"] = self.team.name + result["team_id"] = self.team.public_primary_key + else: + result["team"] = "General" + return result + + @property + def insight_logs_metadata(self): + result = {} + if self.team: + result["team"] = self.team.name + result["team_id"] = self.team.public_primary_key + else: + result["team"] = "General" + return result + @receiver(post_save, sender=AlertReceiveChannel) def listen_for_alertreceivechannel_model_save(sender, instance, created, *args, **kwargs): @@ -665,30 +654,15 @@ def listen_for_alertreceivechannel_model_save(sender, instance, created, *args, IntegrationHeartBeat = apps.get_model("heartbeat", "IntegrationHeartBeat") if created: - description = f"New integration {instance.verbal_name} was created" - create_organization_log( - instance.organization, - instance.author, - type=OrganizationLogType.TYPE_INTEGRATION_CREATED, - description=description, - ) + write_resource_insight_log(instance=instance, author=instance.author, event=EntityEvent.CREATED) default_filter = ChannelFilter(alert_receive_channel=instance, filtering_term=None, is_default=True) default_filter.save() - filter_verbal = default_filter.verbal_name_for_clients.capitalize() - description = f"{filter_verbal} was created for integration {instance.verbal_name}" - create_organization_log( - instance.organization, - None, - OrganizationLogType.TYPE_CHANNEL_FILTER_CREATED, - description, - ) + write_resource_insight_log(instance=default_filter, author=instance.author, event=EntityEvent.CREATED) + TEN_MINUTES = 600 # this is timeout for cloud heartbeats if instance.is_available_for_integration_heartbeat: - IntegrationHeartBeat.objects.create(alert_receive_channel=instance, timeout_seconds=TEN_MINUTES) - description = f"Heartbeat for integration {instance.verbal_name} was created" - create_organization_log( - instance.organization, None, OrganizationLogType.TYPE_HEARTBEAT_CREATED, description - ) + heartbeat = IntegrationHeartBeat.objects.create(alert_receive_channel=instance, timeout_seconds=TEN_MINUTES) + write_resource_insight_log(instance=heartbeat, author=instance.author, event=EntityEvent.CREATED) if instance.integration == AlertReceiveChannel.INTEGRATION_GRAFANA_ALERTING: if created: diff --git a/engine/apps/alerts/models/channel_filter.py b/engine/apps/alerts/models/channel_filter.py index fb369088..2d573714 100644 --- a/engine/apps/alerts/models/channel_filter.py +++ b/engine/apps/alerts/models/channel_filter.py @@ -129,45 +129,57 @@ class ChannelFilter(OrderedModel): else: return self.slack_channel_id - @property - def repr_settings_for_client_side_logging(self): - """ - Example of execution: - term: .*, order: 0, slack notification allowed: Yes, telegram notification allowed: Yes, - slack channel: without_amixr_general_channel, telegram channel: default - """ - result = ( - f"term: {self.str_for_clients}, order: {self.order}, slack notification allowed: " - f"{'Yes' if self.notify_in_slack else 'No'}, telegram notification allowed: " - f"{'Yes' if self.notify_in_telegram else 'No'}" - ) - if self.notification_backends: - for backend_id, backend in self.notification_backends.items(): - result += f", {backend_id} notification allowed: {'Yes' if backend.get('enabled') else 'No'}" - slack_channel = None - if self.slack_channel_id: - SlackChannel = apps.get_model("slack", "SlackChannel") - sti = self.alert_receive_channel.organization.slack_team_identity - slack_channel = SlackChannel.objects.filter(slack_team_identity=sti, slack_id=self.slack_channel_id).first() - result += f", slack channel: {slack_channel.name if slack_channel else 'default'}" - result += f", telegram channel: {self.telegram_channel.channel_name if self.telegram_channel else 'default'}" - if self.notification_backends: - for backend_id, backend in self.notification_backends.items(): - channel = backend.get("channel_id") or "default" - result += f", {backend_id} channel: {channel}" - result += f", escalation chain: {self.escalation_chain.name if self.escalation_chain else 'not selected'}" - return result - @property def str_for_clients(self): if self.filtering_term is None: return "default" return str(self.filtering_term).replace("`", "") - @property - def verbal_name_for_clients(self): - return "default route" if self.is_default else f"route `{self.str_for_clients}`" - def send_demo_alert(self): integration = self.alert_receive_channel integration.send_demo_alert(force_route_id=self.pk) + + # Insight logs + @property + def insight_logs_type_verbal(self): + return "route" + + @property + def insight_logs_verbal(self): + return f"{self.str_for_clients} for {self.alert_receive_channel.insight_logs_verbal}" + + @property + def insight_logs_serialized(self): + result = { + "filtering_term": self.str_for_clients, + "order": self.order, + "slack_notification_enabled": self.notify_in_slack, + "telegram_notification_enabled": self.notify_in_telegram, + # TODO: use names instead of pks, it's needed to rework messaging backends for that + } + # TODO: use names instead of pks, it's needed to rework messaging backends for that + if self.slack_channel_id: + if self.slack_channel_id: + SlackChannel = apps.get_model("slack", "SlackChannel") + sti = self.alert_receive_channel.organization.slack_team_identity + slack_channel = SlackChannel.objects.filter( + slack_team_identity=sti, slack_id=self.slack_channel_id + ).first() + result["slack_channel"] = slack_channel.name + if self.telegram_channel: + result["telegram_channel"] = self.telegram_channel.public_primary_key + if self.escalation_chain: + result["escalation_chain"] = self.escalation_chain.insight_logs_verbal + result["escalation_chain_id"] = self.escalation_chain.public_primary_key + if self.notification_backends: + for backend_id, backend in self.notification_backends.items(): + channel = backend.get("channel_id") or "default" + result[backend_id] = channel + return result + + @property + def insight_logs_metadata(self): + return { + "integration": self.alert_receive_channel.insight_logs_verbal, + "integration_id": self.alert_receive_channel.public_primary_key, + } diff --git a/engine/apps/alerts/models/custom_button.py b/engine/apps/alerts/models/custom_button.py index 0ecf1eae..f5784f8e 100644 --- a/engine/apps/alerts/models/custom_button.py +++ b/engine/apps/alerts/models/custom_button.py @@ -94,19 +94,6 @@ class CustomButton(models.Model): def hard_delete(self): super().delete() - @property - def repr_settings_for_client_side_logging(self): - """ - Example of execution: - name: example, team: example, webhook: https://example.com, user: None, password: None, - authorization header: None, data: None - """ - return ( - f"name: {self.name}, team: {self.team.name if self.team else 'No team'}, webhook: {self.webhook}, " - f"user: {self.user}, password: {self.password}, authorization header: {self.authorization_header}, " - f"data: {self.data}, forward_whole_payload {self.forward_whole_payload}" - ) - def build_post_kwargs(self, alert): post_kwargs = {} if self.user and self.password: @@ -148,6 +135,44 @@ class CustomButton(models.Model): """ return json.dumps(string)[1:-1] + # Insight logs + @property + def insight_logs_type_verbal(self): + return "outgoing_webhook" + + @property + def insight_logs_verbal(self): + return self.name + + @property + def insight_logs_serialized(self): + result = { + "name": self.name, + "webhook": self.webhook, + "user": self.user, + "password": self.password, + "authorization_header": self.authorization_header, + "data": self.data, + "forward_whole_payload": self.forward_whole_payload, + } + + if self.team: + result["team"] = self.team.name + result["team_id"] = self.team.public_primary_key + else: + result["team"] = "General" + return result + + @property + def insight_logs_metadata(self): + result = {} + if self.team: + result["team"] = self.team.name + result["team_id"] = self.team.public_primary_key + else: + result["team"] = "General" + return result + class EscapeDoubleQuotesDict(dict): """ diff --git a/engine/apps/alerts/models/escalation_chain.py b/engine/apps/alerts/models/escalation_chain.py index 08ff7f3f..9fe65424 100644 --- a/engine/apps/alerts/models/escalation_chain.py +++ b/engine/apps/alerts/models/escalation_chain.py @@ -46,10 +46,6 @@ class EscalationChain(models.Model): def __str__(self): return f"{self.pk}: {self.name}" - @property - def repr_settings_for_client_side_logging(self): - return f"name: {self.name}, team: {self.team.name if self.team else 'No team'}" - def make_copy(self, copy_name: str): with transaction.atomic(): copied_chain = EscalationChain.objects.create( @@ -68,3 +64,35 @@ class EscalationChain(models.Model): escalation_policy.save() escalation_policy.notify_to_users_queue.set(notify_to_users_queue) return copied_chain + + # Insight logs + @property + def insight_logs_type_verbal(self): + return "escalation_chain" + + @property + def insight_logs_verbal(self): + return self.name + + @property + def insight_logs_serialized(self): + result = { + "name": self.name, + } + + if self.team: + result["team"] = self.team.name + result["team_id"] = self.team.public_primary_key + else: + result["team"] = "General" + return result + + @property + def insight_logs_metadata(self): + result = {} + if self.team: + result["team"] = self.team.name + result["team_id"] = self.team.public_primary_key + else: + result["team"] = "General" + return result diff --git a/engine/apps/alerts/models/escalation_policy.py b/engine/apps/alerts/models/escalation_policy.py index 17e6ac53..cabacb36 100644 --- a/engine/apps/alerts/models/escalation_policy.py +++ b/engine/apps/alerts/models/escalation_policy.py @@ -299,47 +299,6 @@ class EscalationPolicy(OrderedModel): def step_type_verbal(self): return self.STEP_CHOICES[self.step][1] if self.step is not None else "Empty" - @property - def repr_settings_for_client_side_logging(self): - """ - Example of execution: - step: 'Notify multiple Users', order: 0, important: No, users: Alex, Bob - Another example: - step: 'Continue escalation only if time is from', order: 4, from time: 09:40:00 (UTC), to time: 15:40:00 (UTC) - """ - result = f"step: '{self.step_type_verbal}', order: {self.order}" - if self.step not in EscalationPolicy.STEPS_WITH_NO_IMPORTANT_VERSION_SET: - result += f", important: {'Yes' if self.step in EscalationPolicy.IMPORTANT_STEPS_SET else 'No'}" - if self.step == EscalationPolicy.STEP_WAIT: - result += f", wait: {self.get_wait_delay_display() if self.wait_delay else 'default'}" - elif self.step in [EscalationPolicy.STEP_NOTIFY_GROUP, EscalationPolicy.STEP_NOTIFY_GROUP_IMPORTANT]: - result += f", user group: {self.notify_to_group.name if self.notify_to_group else 'not selected'}" - elif self.step in [EscalationPolicy.STEP_NOTIFY_SCHEDULE, EscalationPolicy.STEP_NOTIFY_SCHEDULE_IMPORTANT]: - result += f", on-call schedule: {self.notify_schedule.name if self.notify_schedule else 'not selected'}" - elif self.step == EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON: - result += f", action: {self.custom_button_trigger.name if self.custom_button_trigger else 'not selected'}" - elif self.step in [ - EscalationPolicy.STEP_NOTIFY_USERS_QUEUE, - EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS, - EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS_IMPORTANT, - ]: - if self.notify_to_users_queue: - users_verbal = ", ".join([user.username for user in self.sorted_users_queue]) - else: - users_verbal = "not selected" - result += f", users: {users_verbal}" - elif self.step == EscalationPolicy.STEP_NOTIFY_IF_TIME: - if self.from_time: - from_time_verbal = self.from_time.isoformat() + " (UTC)" - else: - from_time_verbal = "not selected" - if self.to_time: - to_time_verbal = self.to_time.isoformat() + " (UTC)" - else: - to_time_verbal = "not selected" - result += f", from time: {from_time_verbal}, to time: {to_time_verbal}" - return result - @property def sorted_users_queue(self): return sorted(self.notify_to_users_queue.all(), key=lambda user: (user.username or "", user.pk)) @@ -359,3 +318,57 @@ class EscalationPolicy(OrderedModel): step_name = step_choice[1] break return step_name + + # Insight logs + @property + def insight_logs_type_verbal(self): + return "escalation_policy" + + @property + def insight_logs_verbal(self): + return f"Escalation Policy {self.order} in {self.escalation_chain.insight_logs_verbal}" + + @property + def insight_logs_serialized(self): + result = { + "type": self.step_type_verbal, + "order": self.order, + } + + if self.step == EscalationPolicy.STEP_WAIT: + if self.wait_delay: + result["wait_delay"] = self.get_wait_delay_display() + elif self.step in [EscalationPolicy.STEP_NOTIFY_GROUP, EscalationPolicy.STEP_NOTIFY_GROUP_IMPORTANT]: + if self.notify_to_group: + result["user_group"] = self.notify_to_group.name + result["user_group_id"] = self.notify_to_group.public_primary_key + elif self.step in [EscalationPolicy.STEP_NOTIFY_SCHEDULE, EscalationPolicy.STEP_NOTIFY_SCHEDULE_IMPORTANT]: + if self.notify_schedule: + result["on-call_schedule"] = self.notify_schedule.insight_logs_verbal + result["on-call_schedule_id"] = self.notify_schedule.public_primary_key + elif self.step == EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON: + if self.custom_button_trigger: + result["outgoing_webhook"] = self.custom_button_trigger.insight_logs_verbal + result["outgoing_webhook_id"] = self.custom_button_trigger.public_primary_key + elif self.step in [ + EscalationPolicy.STEP_NOTIFY_USERS_QUEUE, + EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS, + EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS_IMPORTANT, + ]: + if self.notify_to_users_queue: + result["notify_users"] = [user.username for user in self.sorted_users_queue] + result["notify_users_ids"] = [user.public_primary_key for user in self.sorted_users_queue] + elif self.step == EscalationPolicy.STEP_NOTIFY_IF_TIME: + if self.from_time: + result["from_time"] = self.from_time.isoformat() + " (UTC)" + if self.to_time: + result["to_time"] = self.to_time.isoformat() + " (UTC)" + + return result + + @property + def insight_logs_metadata(self): + return { + "escalation_chain": self.escalation_chain.insight_logs_verbal, + "escalation_chain_id": self.escalation_chain.public_primary_key, + } diff --git a/engine/apps/alerts/models/maintainable_object.py b/engine/apps/alerts/models/maintainable_object.py index 9bf1d8cd..750444e4 100644 --- a/engine/apps/alerts/models/maintainable_object.py +++ b/engine/apps/alerts/models/maintainable_object.py @@ -7,8 +7,8 @@ from django.db import models, transaction from django.utils import timezone from apps.slack.scenarios.scenario_step import ScenarioStep -from apps.user_management.organization_log_creator import create_organization_log from common.exceptions import MaintenanceCouldNotBeStartedError +from common.insight_log import MaintenanceEvent, write_maintenance_insight_log class MaintainableObject(models.Model): @@ -82,7 +82,6 @@ class MaintainableObject(models.Model): AlertGroup = apps.get_model("alerts", "AlertGroup") AlertReceiveChannel = apps.get_model("alerts", "AlertReceiveChannel") Alert = apps.get_model("alerts", "Alert") - OrganizationLogRecord = apps.get_model("base", "OrganizationLogRecord") with transaction.atomic(): _self = self.__class__.objects.select_for_update().get(pk=self.pk) @@ -105,6 +104,7 @@ class MaintainableObject(models.Model): organization=organization, team=team, integration=AlertReceiveChannel.INTEGRATION_MAINTENANCE, + author=user, ) maintenance_uuid = _self.start_disable_maintenance_task(maintenance_duration) @@ -152,11 +152,7 @@ class MaintainableObject(models.Model): }, ) alert.save() - # create team log - log_type, object_verbal = OrganizationLogRecord.get_log_type_and_maintainable_object_verbal(self, mode, verbal) - description = f"{self.get_maintenance_mode_display()} of {object_verbal} started for {duration_verbal}" - create_organization_log(organization, user, log_type, description) - + write_maintenance_insight_log(self, user, MaintenanceEvent.STARTED) if mode == AlertReceiveChannel.MAINTENANCE: self.send_maintenance_incident(organization, group, alert) self.notify_about_maintenance_action( diff --git a/engine/apps/alerts/tasks/maintenance.py b/engine/apps/alerts/tasks/maintenance.py index bdfca60a..6c296248 100644 --- a/engine/apps/alerts/tasks/maintenance.py +++ b/engine/apps/alerts/tasks/maintenance.py @@ -4,8 +4,8 @@ from django.db import transaction from django.db.models import ExpressionWrapper, F, fields from django.utils import timezone -from apps.user_management.organization_log_creator import create_organization_log from common.custom_celery_tasks import shared_dedicated_queue_retry_task +from common.insight_log import MaintenanceEvent, write_maintenance_insight_log from .task_logger import task_logger @@ -15,7 +15,6 @@ from .task_logger import task_logger ) def disable_maintenance(*args, **kwargs): AlertGroup = apps.get_model("alerts", "AlertGroup") - OrganizationLogRecord = apps.get_model("base", "OrganizationLogRecord") User = apps.get_model("user_management", "User") Organization = apps.get_model("user_management", "Organization") user = None @@ -25,7 +24,6 @@ def disable_maintenance(*args, **kwargs): user = User.objects.get(pk=user_id) force = kwargs.get("force", False) - with transaction.atomic(): if "alert_receive_channel_id" in kwargs: AlertReceiveChannel = apps.get_model("alerts", "AlertReceiveChannel") @@ -52,23 +50,8 @@ def disable_maintenance(*args, **kwargs): if object_under_maintenance is not None and ( disable_maintenance.request.id == object_under_maintenance.maintenance_uuid or force ): - verbal = object_under_maintenance.get_verbal() - log_type, object_verbal = OrganizationLogRecord.get_log_type_and_maintainable_object_verbal( - object_under_maintenance, - object_under_maintenance.maintenance_mode, - verbal, - stopped=True, - ) - description = ( - f"{object_under_maintenance.get_maintenance_mode_display()} of {object_verbal} " - f"stopped{' by user' if user else ''}" - ) - organization = ( - object_under_maintenance - if isinstance(object_under_maintenance, Organization) - else object_under_maintenance.organization - ) - create_organization_log(organization, user, log_type, description) + organization = object_under_maintenance.get_organization() + write_maintenance_insight_log(object_under_maintenance, user, MaintenanceEvent.FINISHED) if object_under_maintenance.maintenance_mode == object_under_maintenance.MAINTENANCE: mode_verbal = "Maintenance" maintenance_incident = AlertGroup.all_objects.get( @@ -82,7 +65,7 @@ def disable_maintenance(*args, **kwargs): if organization.slack_team_identity: transaction.on_commit( lambda: object_under_maintenance.notify_about_maintenance_action( - f"{mode_verbal} of {verbal} finished." + f"{mode_verbal} of {object_under_maintenance.get_verbal()} finished." ) ) diff --git a/engine/apps/alerts/tests/test_channel_filter.py b/engine/apps/alerts/tests/test_channel_filter.py index 69b9c80e..17ebd237 100644 --- a/engine/apps/alerts/tests/test_channel_filter.py +++ b/engine/apps/alerts/tests/test_channel_filter.py @@ -33,29 +33,6 @@ def test_channel_filter_select_filter(make_organization, make_alert_receive_chan assert satisfied_filter == channel_filter -@pytest.mark.django_db -def test_channel_filter_notification_backends_repr(make_organization, make_alert_receive_channel, make_channel_filter): - organization = make_organization() - alert_receive_channel = make_alert_receive_channel(organization) - - # extra backend is enabled - channel_filter = make_channel_filter( - alert_receive_channel, - notification_backends={"BACKEND": {"channel_id": "foobar", "enabled": True}}, - ) - - assert "BACKEND notification allowed: Yes" in channel_filter.repr_settings_for_client_side_logging - assert "BACKEND channel: foobar" in channel_filter.repr_settings_for_client_side_logging - - # backend is disabled - channel_filter_disabled_backend = make_channel_filter( - alert_receive_channel, - notification_backends={"BACKEND": {"channel_id": "foobar", "enabled": False}}, - ) - assert "BACKEND notification allowed: No" in channel_filter_disabled_backend.repr_settings_for_client_side_logging - assert "BACKEND channel: foobar" in channel_filter_disabled_backend.repr_settings_for_client_side_logging - - @mock.patch("apps.integrations.tasks.create_alert.apply_async", return_value=None) @pytest.mark.django_db def test_send_demo_alert( diff --git a/engine/apps/alerts/tests/test_maintenance.py b/engine/apps/alerts/tests/test_maintenance.py index 61e08f05..0db353c3 100644 --- a/engine/apps/alerts/tests/test_maintenance.py +++ b/engine/apps/alerts/tests/test_maintenance.py @@ -22,7 +22,7 @@ def test_start_maintenance_integration( organization, user = maintenance_test_setup alert_receive_channel = make_alert_receive_channel( - organization, integration=AlertReceiveChannel.INTEGRATION_GRAFANA + organization, integration=AlertReceiveChannel.INTEGRATION_GRAFANA, author=user ) mode = AlertReceiveChannel.MAINTENANCE duration = AlertReceiveChannel.DURATION_ONE_HOUR.seconds @@ -43,11 +43,13 @@ def test_start_maintenance_integration_multiple_previous_instances( organization, user = maintenance_test_setup alert_receive_channel = make_alert_receive_channel( - organization, integration=AlertReceiveChannel.INTEGRATION_GRAFANA + organization, integration=AlertReceiveChannel.INTEGRATION_GRAFANA, author=user ) # 2 maintenance integrations were created in the past for i in range(2): - AlertReceiveChannel.create(organization=organization, integration=AlertReceiveChannel.INTEGRATION_MAINTENANCE) + AlertReceiveChannel.create( + organization=organization, integration=AlertReceiveChannel.INTEGRATION_MAINTENANCE, author=user + ) mode = AlertReceiveChannel.MAINTENANCE duration = AlertReceiveChannel.DURATION_ONE_HOUR.seconds @@ -68,7 +70,7 @@ def test_maintenance_integration_will_not_start_twice( organization, user = maintenance_test_setup alert_receive_channel = make_alert_receive_channel( - organization, integration=AlertReceiveChannel.INTEGRATION_GRAFANA + organization, integration=AlertReceiveChannel.INTEGRATION_GRAFANA, author=user ) mode = AlertReceiveChannel.MAINTENANCE duration = AlertReceiveChannel.DURATION_ONE_HOUR.seconds diff --git a/engine/apps/api/serializers/organization.py b/engine/apps/api/serializers/organization.py index 4868ee40..49179049 100644 --- a/engine/apps/api/serializers/organization.py +++ b/engine/apps/api/serializers/organization.py @@ -147,7 +147,7 @@ class CurrentOrganizationSerializer(OrganizationSerializer): else: verbal_time_saved_by_amixr = None - res = { + result = { "grouped_percent": obj.cached_grouped_percent, "alerts_count": obj.cached_alerts_count, "noise_reduction": obj.cached_noise_reduction, @@ -155,7 +155,7 @@ class CurrentOrganizationSerializer(OrganizationSerializer): "verbal_time_saved_by_amixr": verbal_time_saved_by_amixr, } - return res + return result def update(self, instance, validated_data): current_archive_date = instance.archive_alerts_from diff --git a/engine/apps/api/serializers/organization_log_record.py b/engine/apps/api/serializers/organization_log_record.py deleted file mode 100644 index feb52f50..00000000 --- a/engine/apps/api/serializers/organization_log_record.py +++ /dev/null @@ -1,38 +0,0 @@ -from emoji import emojize -from rest_framework import serializers - -from apps.base.models import OrganizationLogRecord -from common.api_helpers.mixins import EagerLoadingMixin - - -class OrganizationLogRecordSerializer(EagerLoadingMixin, serializers.ModelSerializer): - id = serializers.CharField(read_only=True, source="public_primary_key") - author = serializers.SerializerMethodField() - description = serializers.SerializerMethodField() - - class Meta: - model = OrganizationLogRecord - fields = [ - "id", - "author", - "created_at", - "description", - "labels", - ] - - read_only_fields = fields.copy() - - PREFETCH_RELATED = [ - "author__organization", - # "author__slack_user_identities__slack_team_identity__amixr_team", - ] - - SELECT_RELATED = ["author", "organization"] - - def get_author(self, obj): - if obj.author: - user_data = obj.author.short() - return user_data - - def get_description(self, obj): - return emojize(obj.description, use_aliases=True).replace("\n", "
") diff --git a/engine/apps/api/tests/test_organization_log_record.py b/engine/apps/api/tests/test_organization_log_record.py deleted file mode 100644 index a27d45ae..00000000 --- a/engine/apps/api/tests/test_organization_log_record.py +++ /dev/null @@ -1,242 +0,0 @@ -from unittest.mock import patch - -import pytest -from django.urls import reverse -from rest_framework import status -from rest_framework.response import Response -from rest_framework.test import APIClient - -from apps.base.models import OrganizationLogRecord -from apps.user_management.organization_log_creator import OrganizationLogType -from common.constants.role import Role - - -@pytest.mark.django_db -@pytest.mark.parametrize( - "role,expected_status", - [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), - ], -) -def test_organization_log_records_permissions( - make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status -): - _, user, token = make_organization_and_user_with_plugin_token(role) - client = APIClient() - url = reverse("api-internal:organization_log-list") - - with patch( - "apps.api.views.organization_log_record.OrganizationLogRecordView.list", - return_value=Response( - status=status.HTTP_200_OK, - ), - ): - response = client.get(url, format="json", **make_user_auth_headers(user, token)) - assert response.status_code == expected_status - - -@pytest.mark.django_db -@pytest.mark.parametrize( - "role,expected_status", - [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), - ], -) -def test_organization_log_records_filters_permissions( - make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status -): - _, user, token = make_organization_and_user_with_plugin_token(role) - client = APIClient() - url = reverse("api-internal:organization_log-filters") - - with patch( - "apps.api.views.organization_log_record.OrganizationLogRecordView.filters", - return_value=Response( - status=status.HTTP_200_OK, - ), - ): - response = client.get(url, format="json", **make_user_auth_headers(user, token)) - assert response.status_code == expected_status - - -@pytest.mark.django_db -@pytest.mark.parametrize( - "role,expected_status", - [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), - ], -) -def test_organization_log_records_label_options_permissions( - make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status -): - _, user, token = make_organization_and_user_with_plugin_token(role) - client = APIClient() - url = reverse("api-internal:organization_log-label-options") - - with patch( - "apps.api.views.organization_log_record.OrganizationLogRecordView.label_options", - return_value=Response( - status=status.HTTP_200_OK, - ), - ): - response = client.get(url, format="json", **make_user_auth_headers(user, token)) - assert response.status_code == expected_status - - -@pytest.mark.django_db -def test_get_filter_created_at( - make_organization_and_user_with_plugin_token, - make_organization_log_record, - make_user_auth_headers, -): - organization, user, token = make_organization_and_user_with_plugin_token() - client = APIClient() - make_organization_log_record(organization, user) - - url = reverse("api-internal:organization_log-list") - response = client.get( - url + "?created_at=1970-01-01T00:00:00/2099-01-01T23:59:59", - format="json", - **make_user_auth_headers(user, token), - ) - assert response.status_code == status.HTTP_200_OK - assert len(response.data["results"]) == 1 - - -@pytest.mark.django_db -def test_get_filter_created_at_empty_result( - make_organization_and_user_with_plugin_token, - make_organization_log_record, - make_user_auth_headers, -): - organization, user, token = make_organization_and_user_with_plugin_token() - client = APIClient() - make_organization_log_record(organization, user) - - url = reverse("api-internal:organization_log-list") - response = client.get( - f"{url}?created_at=1970-01-01T00:00:00/1970-01-01T23:59:59", - format="json", - **make_user_auth_headers(user, token), - ) - assert response.status_code == status.HTTP_200_OK - assert len(response.data["results"]) == 0 - - -@pytest.mark.django_db -def test_get_filter_created_at_invalid_format( - make_organization_and_user_with_plugin_token, - make_user_auth_headers, -): - organization, user, token = make_organization_and_user_with_plugin_token() - client = APIClient() - url = reverse("api-internal:organization_log-list") - response = client.get(f"{url}?created_at=invalid_date_format", format="json", **make_user_auth_headers(user, token)) - assert response.status_code == status.HTTP_400_BAD_REQUEST - - -@pytest.mark.django_db -def test_get_filter_by_labels( - make_organization_and_user_with_plugin_token, - make_organization_log_record, - make_user_auth_headers, -): - organization, user, token = make_organization_and_user_with_plugin_token() - client = APIClient() - - # create log that contains LABEL_SLACK and LABEL_DEFAULT_CHANNEL - make_organization_log_record(organization, user, type=OrganizationLogType.TYPE_SLACK_DEFAULT_CHANNEL_CHANGED) - # create log that contains LABEL_SLACK but does not contain LABEL_DEFAULT_CHANNEL - make_organization_log_record(organization, user, type=OrganizationLogType.TYPE_SLACK_WORKSPACE_DISCONNECTED) - # create log that does not contain labels from search - make_organization_log_record(organization, user, type=OrganizationLogType.TYPE_INTEGRATION_CREATED) - - url = reverse("api-internal:organization_log-list") - # search by one label: LABEL_SLACK - response = client.get( - f"{url}?labels={OrganizationLogRecord.LABEL_SLACK}", format="json", **make_user_auth_headers(user, token) - ) - assert response.status_code == status.HTTP_200_OK - assert len(response.data["results"]) == 2 - response_log_labels = [log["labels"] for log in response.data["results"]] - for labels in response_log_labels: - assert OrganizationLogRecord.LABEL_SLACK in labels - - # search by two labels: LABEL_SLACK and LABEL_DEFAULT_CHANNEL - response = client.get( - f"{url}?labels={OrganizationLogRecord.LABEL_SLACK}&labels={OrganizationLogRecord.LABEL_DEFAULT_CHANNEL}", - format="json", - **make_user_auth_headers(user, token), - ) - assert response.status_code == status.HTTP_200_OK - assert len(response.data["results"]) == 1 - response_log_labels = [log["labels"] for log in response.data["results"]] - for labels in response_log_labels: - assert OrganizationLogRecord.LABEL_SLACK in labels - assert OrganizationLogRecord.LABEL_DEFAULT_CHANNEL in labels - - -@pytest.mark.django_db -def test_get_filter_author( - make_organization_and_user_with_plugin_token, - make_user_for_organization, - make_organization_log_record, - make_user_auth_headers, -): - client = APIClient() - - organization, first_user, token = make_organization_and_user_with_plugin_token() - second_user = make_user_for_organization(organization) - make_organization_log_record(organization, first_user) - - url = reverse("api-internal:organization_log-list") - first_response = client.get( - f"{url}?author={first_user.public_primary_key}", format="json", **make_user_auth_headers(first_user, token) - ) - assert first_response.status_code == status.HTTP_200_OK - assert len(first_response.data["results"]) == 1 - - second_response = client.get( - f"{url}?author={second_user.public_primary_key}", format="json", **make_user_auth_headers(first_user, token) - ) - assert second_response.status_code == status.HTTP_200_OK - assert len(second_response.data["results"]) == 0 - - -@pytest.mark.django_db -def test_get_filter_author_multiple_values( - make_organization_and_user_with_plugin_token, - make_user_for_organization, - make_organization_log_record, - make_user_auth_headers, -): - client = APIClient() - - organization, first_user, token = make_organization_and_user_with_plugin_token() - second_user = make_user_for_organization(organization) - third_user = make_user_for_organization(organization) - make_organization_log_record(organization, first_user) - make_organization_log_record(organization, second_user) - - url = reverse("api-internal:organization_log-list") - first_response = client.get( - f"{url}?author={first_user.public_primary_key}&author={second_user.public_primary_key}", - format="json", - **make_user_auth_headers(first_user, token), - ) - assert first_response.status_code == status.HTTP_200_OK - assert len(first_response.data["results"]) == 2 - - second_response = client.get( - f"{url}?author={first_user.public_primary_key}&author={third_user.public_primary_key}", - format="json", - **make_user_auth_headers(first_user, token), - ) - assert second_response.status_code == status.HTTP_200_OK - assert len(second_response.data["results"]) == 1 diff --git a/engine/apps/api/urls.py b/engine/apps/api/urls.py index a4c585c3..3b7f46bf 100644 --- a/engine/apps/api/urls.py +++ b/engine/apps/api/urls.py @@ -25,7 +25,6 @@ from .views.organization import ( GetTelegramVerificationCode, SetGeneralChannel, ) -from .views.organization_log_record import OrganizationLogRecordView from .views.preview_template_options import PreviewTemplateOptionsView from .views.public_api_tokens import PublicApiTokenView from .views.resolution_note import ResolutionNoteView @@ -65,7 +64,6 @@ router.register(r"telegram_channels", TelegramChannelViewSet, basename="telegram router.register(r"slack_channels", SlackChannelView, basename="slack_channel") router.register(r"user_groups", UserGroupViewSet, basename="user_group") router.register(r"heartbeats", IntegrationHeartBeatView, basename="integration_heartbeat") -router.register(r"organization_logs", OrganizationLogRecordView, basename="organization_log") router.register(r"tokens", PublicApiTokenView, basename="api_token") router.register(r"live_settings", LiveSettingViewSet, basename="live_settings") router.register(r"oncall_shifts", OnCallShiftView, basename="oncall_shifts") diff --git a/engine/apps/api/views/alert_receive_channel.py b/engine/apps/api/views/alert_receive_channel.py index ba20565e..6dce1c56 100644 --- a/engine/apps/api/views/alert_receive_channel.py +++ b/engine/apps/api/views/alert_receive_channel.py @@ -17,7 +17,6 @@ from apps.api.serializers.alert_receive_channel import ( ) from apps.api.throttlers import DemoAlertThrottler from apps.auth_token.auth import PluginAuthentication -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.exceptions import BadRequest from common.api_helpers.mixins import ( FilterSerializerMixin, @@ -26,6 +25,7 @@ from common.api_helpers.mixins import ( UpdateSerializerMixin, ) from common.exceptions import TeamCanNotBeChangedError, UnableToSendDemoAlert +from common.insight_log import EntityEvent, write_resource_insight_log class AlertReceiveChannelFilter(filters.FilterSet): @@ -96,21 +96,22 @@ class AlertReceiveChannelView( return Response(data="invalid integration", status=status.HTTP_400_BAD_REQUEST) def perform_update(self, serializer): - old_state = serializer.instance.repr_settings_for_client_side_logging + prev_state = serializer.instance.insight_logs_serialized serializer.save() - new_state = serializer.instance.repr_settings_for_client_side_logging - description = f"Integration settings was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log( - serializer.instance.organization, - self.request.user, - OrganizationLogType.TYPE_INTEGRATION_CHANGED, - description, + 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): - description = f"Integration {instance.verbal_name} was deleted" - create_organization_log( - instance.organization, self.request.user, OrganizationLogType.TYPE_INTEGRATION_DELETED, description + write_resource_insight_log( + instance=instance, + author=self.request.user, + event=EntityEvent.DELETED, ) instance.delete() diff --git a/engine/apps/api/views/alert_receive_channel_template.py b/engine/apps/api/views/alert_receive_channel_template.py index 2845478d..ff8cd923 100644 --- a/engine/apps/api/views/alert_receive_channel_template.py +++ b/engine/apps/api/views/alert_receive_channel_template.py @@ -5,8 +5,8 @@ from apps.alerts.models import AlertReceiveChannel from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin from apps.api.serializers.alert_receive_channel import AlertReceiveChannelTemplatesSerializer from apps.auth_token.auth import PluginAuthentication -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.mixins import PublicPrimaryKeyMixin +from common.insight_log import EntityEvent, write_resource_insight_log class AlertReceiveChannelTemplateView( @@ -35,18 +35,15 @@ class AlertReceiveChannelTemplateView( def update(self, request, *args, **kwargs): instance = self.get_object() - old_state = instance.repr_settings_for_client_side_logging + prev_state = instance.insight_logs_serialized result = super().update(request, *args, **kwargs) instance = self.get_object() - new_state = instance.repr_settings_for_client_side_logging - - if new_state != old_state: - description = f"Integration settings was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log( - instance.organization, - self.request.user, - OrganizationLogType.TYPE_INTEGRATION_CHANGED, - description, - ) - + new_state = instance.insight_logs_serialized + write_resource_insight_log( + instance=instance, + author=self.request.user, + event=EntityEvent.UPDATED, + prev_state=prev_state, + new_state=new_state, + ) return result diff --git a/engine/apps/api/views/channel_filter.py b/engine/apps/api/views/channel_filter.py index 9d9cc73d..efe397d1 100644 --- a/engine/apps/api/views/channel_filter.py +++ b/engine/apps/api/views/channel_filter.py @@ -15,10 +15,10 @@ from apps.api.serializers.channel_filter import ( from apps.api.throttlers import DemoAlertThrottler from apps.auth_token.auth import PluginAuthentication from apps.slack.models import SlackChannel -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.exceptions import BadRequest from common.api_helpers.mixins import CreateSerializerMixin, PublicPrimaryKeyMixin, UpdateSerializerMixin from common.exceptions import UnableToSendDemoAlert +from common.insight_log import EntityEvent, write_resource_insight_log class ChannelFilterView(PublicPrimaryKeyMixin, CreateSerializerMixin, UpdateSerializerMixin, ModelViewSet): @@ -59,70 +59,59 @@ class ChannelFilterView(PublicPrimaryKeyMixin, CreateSerializerMixin, UpdateSeri return queryset def destroy(self, request, *args, **kwargs): - user = request.user instance = self.get_object() if instance.is_default: raise BadRequest(detail="Unable to delete default filter") else: - alert_receive_channel = instance.alert_receive_channel - route_verbal = instance.verbal_name_for_clients.capitalize() - description = f"{route_verbal} for integration {alert_receive_channel.verbal_name} was deleted" - create_organization_log( - user.organization, user, OrganizationLogType.TYPE_CHANNEL_FILTER_DELETED, description + write_resource_insight_log( + instance=instance, + author=self.request.user, + event=EntityEvent.DELETED, ) self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) def perform_create(self, serializer): - user = self.request.user serializer.save() - instance = serializer.instance - alert_receive_channel = instance.alert_receive_channel - route_verbal = instance.verbal_name_for_clients.capitalize() - description = f"{route_verbal} was created for integration {alert_receive_channel.verbal_name}" - create_organization_log(user.organization, user, OrganizationLogType.TYPE_CHANNEL_FILTER_CREATED, description) + write_resource_insight_log( + instance=serializer.instance, + author=self.request.user, + event=EntityEvent.CREATED, + ) def perform_update(self, serializer): - user = self.request.user - old_state = serializer.instance.repr_settings_for_client_side_logging + prev_state = serializer.instance.insight_logs_serialized serializer.save() - new_state = serializer.instance.repr_settings_for_client_side_logging - alert_receive_channel = serializer.instance.alert_receive_channel - route_verbal = serializer.instance.verbal_name_for_clients - description = ( - f"Settings for {route_verbal} of integration {alert_receive_channel.verbal_name} " - f"was changed from:\n{old_state}\nto:\n{new_state}" + 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, ) - create_organization_log(user.organization, user, OrganizationLogType.TYPE_CHANNEL_FILTER_CHANGED, description) @action(detail=True, methods=["put"]) def move_to_position(self, request, pk): position = request.query_params.get("position", None) if position is not None: try: - source_filter = ChannelFilter.objects.get(public_primary_key=pk) + instance = ChannelFilter.objects.get(public_primary_key=pk) except ChannelFilter.DoesNotExist: raise BadRequest(detail="Channel filter does not exist") try: - if source_filter.is_default: + if instance.is_default: raise BadRequest(detail="Unable to change position for default filter") - user = self.request.user - old_state = source_filter.repr_settings_for_client_side_logging + prev_state = instance.insight_logs_serialized + instance.to(int(position)) + new_state = instance.insight_logs_serialized - source_filter.to(int(position)) - - new_state = source_filter.repr_settings_for_client_side_logging - alert_receive_channel = source_filter.alert_receive_channel - route_verbal = source_filter.verbal_name_for_clients - description = ( - f"Settings for {route_verbal} of integration {alert_receive_channel.verbal_name} " - f"was changed from:\n{old_state}\nto:\n{new_state}" - ) - create_organization_log( - user.organization, - user, - OrganizationLogType.TYPE_CHANNEL_FILTER_CHANGED, - description, + write_resource_insight_log( + instance=instance, + author=self.request.user, + event=EntityEvent.UPDATED, + prev_state=prev_state, + new_state=new_state, ) return Response(status=status.HTTP_200_OK) except ValueError as e: diff --git a/engine/apps/api/views/custom_button.py b/engine/apps/api/views/custom_button.py index 91901f71..0a9f1973 100644 --- a/engine/apps/api/views/custom_button.py +++ b/engine/apps/api/views/custom_button.py @@ -11,9 +11,9 @@ from apps.alerts.tasks.custom_button_result import custom_button_result from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin, IsAdminOrEditor from apps.api.serializers.custom_button import CustomButtonSerializer from apps.auth_token.auth import PluginAuthentication -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.exceptions import BadRequest from common.api_helpers.mixins import PublicPrimaryKeyMixin +from common.insight_log import EntityEvent, write_resource_insight_log class CustomButtonView(PublicPrimaryKeyMixin, ModelViewSet): @@ -55,26 +55,30 @@ class CustomButtonView(PublicPrimaryKeyMixin, ModelViewSet): def perform_create(self, serializer): serializer.save() - instance = serializer.instance - organization = self.request.auth.organization - user = self.request.user - description = f"Custom action {instance.name} was created" - create_organization_log(organization, user, OrganizationLogType.TYPE_CUSTOM_ACTION_CREATED, description) + write_resource_insight_log( + instance=serializer.instance, + author=self.request.user, + event=EntityEvent.CREATED, + ) def perform_update(self, serializer): - organization = self.request.auth.organization - user = self.request.user - old_state = serializer.instance.repr_settings_for_client_side_logging + prev_state = serializer.instance.insight_logs_serialized serializer.save() - new_state = serializer.instance.repr_settings_for_client_side_logging - description = f"Custom action {serializer.instance.name} was changed " f"from:\n{old_state}\nto:\n{new_state}" - create_organization_log(organization, user, OrganizationLogType.TYPE_CUSTOM_ACTION_CHANGED, description) + 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): - organization = self.request.auth.organization - user = self.request.user - description = f"Custom action {instance.name} was deleted" - create_organization_log(organization, user, OrganizationLogType.TYPE_CUSTOM_ACTION_DELETED, description) + write_resource_insight_log( + instance=instance, + author=self.request.user, + event=EntityEvent.DELETED, + ) instance.delete() @action(detail=True, methods=["post"]) diff --git a/engine/apps/api/views/escalation_chain.py b/engine/apps/api/views/escalation_chain.py index 839dcc90..f972a992 100644 --- a/engine/apps/api/views/escalation_chain.py +++ b/engine/apps/api/views/escalation_chain.py @@ -10,9 +10,9 @@ from apps.alerts.models import EscalationChain from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin from apps.api.serializers.escalation_chain import EscalationChainListSerializer, EscalationChainSerializer from apps.auth_token.auth import PluginAuthentication -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.exceptions import BadRequest from common.api_helpers.mixins import ListSerializerMixin, PublicPrimaryKeyMixin +from common.insight_log import EntityEvent, write_resource_insight_log class EscalationChainViewSet(PublicPrimaryKeyMixin, ListSerializerMixin, viewsets.ModelViewSet): @@ -56,45 +56,31 @@ class EscalationChainViewSet(PublicPrimaryKeyMixin, ListSerializerMixin, viewset def perform_create(self, serializer): serializer.save() - - instance = serializer.instance - description = f"Escalation chain {instance.name} was created" - create_organization_log( - instance.organization, - self.request.user, - OrganizationLogType.TYPE_ESCALATION_CHAIN_CREATED, - description, - ) + write_resource_insight_log(instance=serializer.instance, author=self.request.user, event=EntityEvent.CREATED) def perform_destroy(self, instance): + write_resource_insight_log( + instance=instance, + author=self.request.user, + event=EntityEvent.DELETED, + ) instance.delete() - description = f"Escalation chain {instance.name} was deleted" - create_organization_log( - instance.organization, - self.request.user, - OrganizationLogType.TYPE_ESCALATION_CHAIN_DELETED, - description, - ) - def perform_update(self, serializer): - instance = serializer.instance - old_state = instance.repr_settings_for_client_side_logging - + prev_state = serializer.instance.insight_logs_serialized serializer.save() + new_state = serializer.instance.insight_logs_serialized - new_state = instance.repr_settings_for_client_side_logging - description = f"Escalation chain {instance.name} was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log( - instance.organization, - self.request.user, - OrganizationLogType.TYPE_ESCALATION_CHAIN_CHANGED, - description, + write_resource_insight_log( + instance=serializer.instance, + author=self.request.user, + event=EntityEvent.UPDATED, + prev_state=prev_state, + new_state=new_state, ) @action(methods=["post"], detail=True) def copy(self, request, pk): - user = request.user name = request.data.get("name") if name is None: raise BadRequest(detail={"name": ["This field may not be null."]}) @@ -105,8 +91,11 @@ class EscalationChainViewSet(PublicPrimaryKeyMixin, ListSerializerMixin, viewset obj = self.get_object() copy = obj.make_copy(name) serializer = self.get_serializer(copy) - description = f"Escalation chain {obj.name} was copied with new name {name}" - create_organization_log(copy.organization, user, OrganizationLogType.TYPE_CHANNEL_FILTER_CHANGED, description) + write_resource_insight_log( + instance=copy, + author=self.request.user, + event=EntityEvent.CREATED, + ) return Response(serializer.data) @action(methods=["get"], detail=True) diff --git a/engine/apps/api/views/escalation_policy.py b/engine/apps/api/views/escalation_policy.py index b67b7542..c05a2f0e 100644 --- a/engine/apps/api/views/escalation_policy.py +++ b/engine/apps/api/views/escalation_policy.py @@ -14,9 +14,9 @@ from apps.api.serializers.escalation_policy import ( EscalationPolicyUpdateSerializer, ) from apps.auth_token.auth import PluginAuthentication -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.exceptions import BadRequest from common.api_helpers.mixins import CreateSerializerMixin, PublicPrimaryKeyMixin, UpdateSerializerMixin +from common.insight_log import EntityEvent, write_resource_insight_log class EscalationPolicyView(PublicPrimaryKeyMixin, CreateSerializerMixin, UpdateSerializerMixin, ModelViewSet): @@ -66,37 +66,31 @@ class EscalationPolicyView(PublicPrimaryKeyMixin, CreateSerializerMixin, UpdateS def perform_create(self, serializer): serializer.save() - instance = serializer.instance - organization = self.request.user.organization - user = self.request.user - description = ( - f"Escalation step '{instance.step_type_verbal}' with order {instance.order} " - f"was created for escalation chain '{instance.escalation_chain.name}'" + write_resource_insight_log( + instance=serializer.instance, + author=self.request.user, + event=EntityEvent.CREATED, ) - create_organization_log(organization, user, OrganizationLogType.TYPE_ESCALATION_STEP_CREATED, description) def perform_update(self, serializer): - organization = self.request.user.organization - user = self.request.user - old_state = serializer.instance.repr_settings_for_client_side_logging + prev_state = serializer.instance.insight_logs_serialized serializer.save() - new_state = serializer.instance.repr_settings_for_client_side_logging - escalation_chain_name = serializer.instance.escalation_chain.name + new_state = serializer.instance.insight_logs_serialized - description = ( - f"Settings for escalation step of escalation chain '{escalation_chain_name}' " - f"was changed from:\n{old_state}\nto:\n{new_state}" + write_resource_insight_log( + instance=serializer.instance, + author=self.request.user, + event=EntityEvent.UPDATED, + prev_state=prev_state, + new_state=new_state, ) - create_organization_log(organization, user, OrganizationLogType.TYPE_ESCALATION_STEP_CHANGED, description) def perform_destroy(self, instance): - organization = self.request.user.organization - user = self.request.user - description = ( - f"Escalation step '{instance.step_type_verbal}' with order {instance.order} of " - f"of escalation chain '{instance.escalation_chain.name}' was deleted" + write_resource_insight_log( + instance=instance, + author=self.request.user, + event=EntityEvent.DELETED, ) - create_organization_log(organization, user, OrganizationLogType.TYPE_ESCALATION_STEP_DELETED, description) instance.delete() @action(detail=True, methods=["put"]) @@ -104,29 +98,22 @@ class EscalationPolicyView(PublicPrimaryKeyMixin, CreateSerializerMixin, UpdateS position = request.query_params.get("position", None) if position is not None: try: - source_step = EscalationPolicy.objects.get(public_primary_key=pk) + instance = EscalationPolicy.objects.get(public_primary_key=pk) except EscalationPolicy.DoesNotExist: raise BadRequest(detail="Step does not exist") try: - user = self.request.user - old_state = source_step.repr_settings_for_client_side_logging - + prev_state = instance.insight_logs_serialized position = int(position) - source_step.to(position) + instance.to(position) + new_state = instance.insight_logs_serialized - new_state = source_step.repr_settings_for_client_side_logging - escalation_chain_name = source_step.escalation_chain.name - description = ( - f"Settings for escalation step of escalation chain '{escalation_chain_name}' " - f"was changed from:\n{old_state}\nto:\n{new_state}" + write_resource_insight_log( + instance=instance, + author=self.request.user, + event=EntityEvent.UPDATED, + prev_state=prev_state, + new_state=new_state, ) - create_organization_log( - user.organization, - user, - OrganizationLogType.TYPE_ESCALATION_STEP_CHANGED, - description, - ) - return Response(status=status.HTTP_200_OK) except ValueError as e: raise BadRequest(detail=f"{e}") diff --git a/engine/apps/api/views/integration_heartbeat.py b/engine/apps/api/views/integration_heartbeat.py index 6f27db8b..fa50e29b 100644 --- a/engine/apps/api/views/integration_heartbeat.py +++ b/engine/apps/api/views/integration_heartbeat.py @@ -7,8 +7,8 @@ from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, from apps.api.serializers.integration_heartbeat import IntegrationHeartBeatSerializer from apps.auth_token.auth import PluginAuthentication from apps.heartbeat.models import IntegrationHeartBeat -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.mixins import PublicPrimaryKeyMixin +from common.insight_log import EntityEvent, write_resource_insight_log class IntegrationHeartBeatView( @@ -45,29 +45,22 @@ class IntegrationHeartBeatView( def perform_create(self, serializer): serializer.save() instance = serializer.instance - description = f"Heartbeat for integration {instance.alert_receive_channel.verbal_name} was created" - create_organization_log( - instance.alert_receive_channel.organization, - self.request.user, - OrganizationLogType.TYPE_HEARTBEAT_CREATED, - description, + write_resource_insight_log( + instance=instance, + author=self.request.user, + event=EntityEvent.CREATED, ) def perform_update(self, serializer): - old_state = serializer.instance.repr_settings_for_client_side_logging + prev_state = serializer.instance.insight_logs_serialized serializer.save() - new_state = serializer.instance.repr_settings_for_client_side_logging - alert_receive_channel = serializer.instance.alert_receive_channel - description = ( - f"Settings for heartbeat of integration " - f"{alert_receive_channel.verbal_name} was changed " - f"from:\n{old_state}\nto:\n{new_state}" - ) - create_organization_log( - alert_receive_channel.organization, - self.request.user, - OrganizationLogType.TYPE_HEARTBEAT_CHANGED, - description, + 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, ) @action(detail=False, methods=["get"]) diff --git a/engine/apps/api/views/on_call_shifts.py b/engine/apps/api/views/on_call_shifts.py index ad9fe688..5ccce3b4 100644 --- a/engine/apps/api/views/on_call_shifts.py +++ b/engine/apps/api/views/on_call_shifts.py @@ -10,10 +10,10 @@ from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, from apps.api.serializers.on_call_shifts import OnCallShiftSerializer, OnCallShiftUpdateSerializer from apps.auth_token.auth import PluginAuthentication from apps.schedules.models import CustomOnCallShift -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.mixins import PublicPrimaryKeyMixin, UpdateSerializerMixin from common.api_helpers.paginators import FiftyPageSizePaginator from common.api_helpers.utils import get_date_range_from_request +from common.insight_log import EntityEvent, write_resource_insight_log class OnCallShiftView(PublicPrimaryKeyMixin, UpdateSerializerMixin, ModelViewSet): @@ -52,31 +52,30 @@ class OnCallShiftView(PublicPrimaryKeyMixin, UpdateSerializerMixin, ModelViewSet def perform_create(self, serializer): serializer.save() - instance = serializer.instance - organization = self.request.auth.organization - user = self.request.user - description = ( - f"Custom on-call shift with params: {instance.repr_settings_for_client_side_logging} " - f"was created" # todo + write_resource_insight_log( + instance=serializer.instance, + author=self.request.user, + event=EntityEvent.DELETED, ) - create_organization_log(organization, user, OrganizationLogType.TYPE_ON_CALL_SHIFT_CREATED, description) def perform_update(self, serializer): - organization = self.request.auth.organization - user = self.request.user - old_state = serializer.instance.repr_settings_for_client_side_logging + prev_state = serializer.instance.insight_logs_serialized serializer.save() - new_state = serializer.instance.repr_settings_for_client_side_logging - description = f"Settings of custom on-call shift was changed " f"from:\n{old_state}\nto:\n{new_state}" - create_organization_log(organization, user, OrganizationLogType.TYPE_ON_CALL_SHIFT_CHANGED, description) + 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): - organization = self.request.auth.organization - user = self.request.user - description = ( - f"Custom on-call shift " f"with params: {instance.repr_settings_for_client_side_logging} was deleted" + write_resource_insight_log( + instance=instance, + author=self.request.user, + event=EntityEvent.DELETED, ) - create_organization_log(organization, user, OrganizationLogType.TYPE_ON_CALL_SHIFT_DELETED, description) instance.delete() @action(detail=False, methods=["post"]) diff --git a/engine/apps/api/views/organization.py b/engine/apps/api/views/organization.py index 70af2f1d..34580734 100644 --- a/engine/apps/api/views/organization.py +++ b/engine/apps/api/views/organization.py @@ -11,7 +11,7 @@ from apps.api.serializers.organization import CurrentOrganizationSerializer from apps.auth_token.auth import PluginAuthentication from apps.base.messaging import get_messaging_backend_from_id from apps.telegram.client import TelegramClient -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log +from common.insight_log import EntityEvent, write_resource_insight_log class CurrentOrganizationView(APIView): @@ -27,16 +27,19 @@ class CurrentOrganizationView(APIView): def put(self, request): organization = self.request.auth.organization - old_state = organization.repr_settings_for_client_side_logging + prev_state = organization.insight_logs_serialized serializer = CurrentOrganizationSerializer( instance=organization, data=request.data, context={"request": request} ) serializer.is_valid(raise_exception=True) serializer.save() - new_state = serializer.instance.repr_settings_for_client_side_logging - description = f"Organization settings was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log( - organization, request.user, OrganizationLogType.TYPE_ORGANIZATION_SETTINGS_CHANGED, description + 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, ) return Response(serializer.data) diff --git a/engine/apps/api/views/organization_log_record.py b/engine/apps/api/views/organization_log_record.py deleted file mode 100644 index 1d5087ed..00000000 --- a/engine/apps/api/views/organization_log_record.py +++ /dev/null @@ -1,128 +0,0 @@ -from datetime import timedelta - -from django.db.models import Q -from django.utils import timezone -from django_filters import rest_framework as filters -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import mixins, viewsets -from rest_framework.decorators import action -from rest_framework.filters import SearchFilter -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response - -from apps.api.serializers.organization_log_record import OrganizationLogRecordSerializer -from apps.auth_token.auth import PluginAuthentication -from apps.base.models import OrganizationLogRecord -from apps.user_management.models import User -from common.api_helpers.filters import DateRangeFilterMixin, ModelFieldFilterMixin -from common.api_helpers.paginators import FiftyPageSizePaginator - -LABEL_CHOICES = [[label, label] for label in OrganizationLogRecord.LABELS] - - -def get_user_queryset(request): - if request is None: - return User.objects.none() - - return User.objects.filter(organization=request.user.organization).distinct() - - -class OrganizationLogRecordFilter(DateRangeFilterMixin, ModelFieldFilterMixin, filters.FilterSet): - - author = filters.ModelMultipleChoiceFilter( - field_name="author", - queryset=get_user_queryset, - to_field_name="public_primary_key", - method=ModelFieldFilterMixin.filter_model_field.__name__, - ) - created_at = filters.CharFilter(field_name="created_at", method=DateRangeFilterMixin.filter_date_range.__name__) - labels = filters.MultipleChoiceFilter(choices=LABEL_CHOICES, method="filter_labels") - - class Meta: - model = OrganizationLogRecord - fields = ["author", "labels", "created_at"] - - def filter_labels(self, queryset, name, value): - if not value: - return queryset - - q_objects = Q() - for item in value: - q_objects &= Q(_labels__contains=item) - - queryset = queryset.filter(q_objects) - - return queryset - - -class OrganizationLogRecordView(mixins.ListModelMixin, viewsets.GenericViewSet): - authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated,) - - serializer_class = OrganizationLogRecordSerializer - - pagination_class = FiftyPageSizePaginator - - filter_backends = ( - SearchFilter, - DjangoFilterBackend, - ) - search_fields = ("description",) - filterset_class = OrganizationLogRecordFilter - - def get_queryset(self): - queryset = OrganizationLogRecord.objects.filter(organization=self.request.auth.organization).order_by( - "-created_at" - ) - queryset = self.serializer_class.setup_eager_loading(queryset) - return queryset - - @action(detail=False, methods=["get"]) - def filters(self, request): - filter_name = request.query_params.get("filter_name", None) - api_root = "/api/internal/v1/" - - filter_options = [ - { - "name": "search", - "type": "search", - }, - { - "name": "author", - "type": "options", - "href": api_root + "users/?filters=true&roles=0&roles=1&roles=2", - }, - { - "name": "labels", - "type": "options", - "options": [ - { - "display_name": label, - "value": label, - } - for label in OrganizationLogRecord.LABELS - ], - }, - { - "name": "created_at", - "type": "daterange", - "default": f"{timezone.datetime.now() - timedelta(days=7):%Y-%m-%d/{timezone.datetime.now():%Y-%m-%d}}", - }, - ] - - if filter_name is not None: - filter_options = list(filter(lambda f: f["name"].startswith(filter_name), filter_options)) - - return Response(filter_options) - - @action(detail=False, methods=["get"]) - def label_options(self, request): - return Response( - [ - { - "display_name": label, - "value": label, - } - for label in OrganizationLogRecord.LABELS - ] - ) diff --git a/engine/apps/api/views/public_api_tokens.py b/engine/apps/api/views/public_api_tokens.py index 57d09901..36ba4488 100644 --- a/engine/apps/api/views/public_api_tokens.py +++ b/engine/apps/api/views/public_api_tokens.py @@ -7,8 +7,8 @@ from apps.api.serializers.public_api_token import PublicApiTokenSerializer from apps.auth_token.auth import PluginAuthentication from apps.auth_token.constants import MAX_PUBLIC_API_TOKENS_PER_USER from apps.auth_token.models import ApiAuthToken -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.exceptions import BadRequest +from common.insight_log import EntityEvent, write_resource_insight_log class PublicApiTokenView( @@ -30,10 +30,8 @@ class PublicApiTokenView( return ApiAuthToken.objects.filter(user=self.request.user, organization=self.request.user.organization) def destroy(self, request, *args, **kwargs): - user = request.user instance = self.get_object() - description = f"API token {instance.name} was revoked" - create_organization_log(user.organization, user, OrganizationLogType.TYPE_CHANNEL_FILTER_DELETED, description) + write_resource_insight_log(instance=instance, author=instance.author, event=EntityEvent.DELETED) self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) @@ -51,5 +49,5 @@ class PublicApiTokenView( raise BadRequest("Invalid token name") instance, token = ApiAuthToken.create_auth_token(user, user.organization, token_name) data = {"id": instance.pk, "token": token, "name": instance.name, "created_at": instance.created_at} - + write_resource_insight_log(instance=instance, author=user, event=EntityEvent.CREATED) return Response(data, status=status.HTTP_201_CREATED) diff --git a/engine/apps/api/views/schedule.py b/engine/apps/api/views/schedule.py index 8a066cee..c7f97464 100644 --- a/engine/apps/api/views/schedule.py +++ b/engine/apps/api/views/schedule.py @@ -25,7 +25,6 @@ from apps.auth_token.models import ScheduleExportAuthToken from apps.schedules.models import OnCallSchedule from apps.slack.models import SlackChannel from apps.slack.tasks import update_slack_user_group_for_schedules -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.exceptions import BadRequest, Conflict from common.api_helpers.mixins import ( CreateSerializerMixin, @@ -34,6 +33,7 @@ from common.api_helpers.mixins import ( UpdateSerializerMixin, ) from common.api_helpers.utils import create_engine_url, get_date_range_from_request +from common.insight_log import EntityEvent, write_resource_insight_log EVENTS_FILTER_BY_ROTATION = "rotation" EVENTS_FILTER_BY_OVERRIDE = "override" @@ -136,38 +136,32 @@ class ScheduleView( return super().get_object() def perform_create(self, serializer): - schedule = serializer.save() - if schedule.user_group is not None: - update_slack_user_group_for_schedules.apply_async((schedule.user_group.pk,)) - organization = self.request.auth.organization - user = self.request.user - description = f"Schedule {schedule.name} was created" - create_organization_log(organization, user, OrganizationLogType.TYPE_SCHEDULE_CREATED, description) + serializer.save() + write_resource_insight_log(instance=serializer.instance, author=self.request.user, event=EntityEvent.CREATED) def perform_update(self, serializer): - organization = self.request.auth.organization - user = self.request.user - old_schedule = serializer.instance - old_state = old_schedule.repr_settings_for_client_side_logging + prev_state = serializer.instance.insight_logs_serialized old_user_group = serializer.instance.user_group - - updated_schedule = serializer.save() - + serializer.save() if old_user_group is not None: update_slack_user_group_for_schedules.apply_async((old_user_group.pk,)) - - if updated_schedule.user_group is not None and updated_schedule.user_group != old_user_group: - update_slack_user_group_for_schedules.apply_async((updated_schedule.user_group.pk,)) - - new_state = updated_schedule.repr_settings_for_client_side_logging - description = f"Schedule {updated_schedule.name} was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log(organization, user, OrganizationLogType.TYPE_SCHEDULE_CHANGED, description) + if serializer.instance.user_group is not None and serializer.instance.user_group != old_user_group: + update_slack_user_group_for_schedules.apply_async((serializer.instance.user_group.pk,)) + 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): - organization = self.request.auth.organization - user = self.request.user - description = f"Schedule {instance.name} was deleted" - create_organization_log(organization, user, OrganizationLogType.TYPE_SCHEDULE_DELETED, description) + write_resource_insight_log( + instance=instance, + author=self.request.user, + event=EntityEvent.DELETED, + ) instance.delete() if instance.user_group is not None: @@ -331,6 +325,7 @@ class ScheduleView( instance, token = ScheduleExportAuthToken.create_auth_token( request.user, request.user.organization, schedule ) + write_resource_insight_log(instance=instance, author=self.request.user, event=EntityEvent.CREATED) except IntegrityError: raise Conflict("Schedule export token for user already exists") @@ -346,6 +341,7 @@ class ScheduleView( if self.request.method == "DELETE": try: token = ScheduleExportAuthToken.objects.get(user_id=self.request.user.id, schedule_id=schedule.id) + write_resource_insight_log(instance=token, author=self.request.user, event=EntityEvent.DELETED) token.delete() except ScheduleExportAuthToken.DoesNotExist: raise NotFound diff --git a/engine/apps/api/views/slack_team_settings.py b/engine/apps/api/views/slack_team_settings.py index 2afdfe7f..0da7525b 100644 --- a/engine/apps/api/views/slack_team_settings.py +++ b/engine/apps/api/views/slack_team_settings.py @@ -6,7 +6,7 @@ from apps.api.permissions import AnyRole, IsAdmin, MethodPermission from apps.api.serializers.organization_slack_settings import OrganizationSlackSettingsSerializer from apps.auth_token.auth import PluginAuthentication from apps.user_management.models import Organization -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log +from common.insight_log import EntityEvent, write_resource_insight_log class SlackTeamSettingsAPIView(views.APIView): @@ -27,14 +27,17 @@ class SlackTeamSettingsAPIView(views.APIView): def put(self, request): organization = self.request.auth.organization - old_state = organization.repr_settings_for_client_side_logging + prev_state = organization.insight_logs_serialized serializer = self.serializer_class(organization, data=request.data) serializer.is_valid(raise_exception=True) serializer.save() - new_state = serializer.instance.repr_settings_for_client_side_logging - description = f"Organization settings was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log( - organization, request.user, OrganizationLogType.TYPE_ORGANIZATION_SETTINGS_CHANGED, description + 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, ) return Response(serializer.data) diff --git a/engine/apps/api/views/telegram_channels.py b/engine/apps/api/views/telegram_channels.py index 73681cf7..a8d5cdbb 100644 --- a/engine/apps/api/views/telegram_channels.py +++ b/engine/apps/api/views/telegram_channels.py @@ -7,8 +7,8 @@ from rest_framework.response import Response from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin from apps.api.serializers.telegram import TelegramToOrganizationConnectorSerializer from apps.auth_token.auth import PluginAuthentication -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.mixins import PublicPrimaryKeyMixin +from common.insight_log.chatops_insight_logs import ChatOpsEvent, ChatOpsType, write_chatops_insight_log class TelegramChannelViewSet( @@ -41,8 +41,10 @@ class TelegramChannelViewSet( def perform_destroy(self, instance): user = self.request.user - organization = user.organization - - description = f"Telegram channel @{instance.channel_name} was disconnected from organization" - create_organization_log(organization, user, OrganizationLogType.TYPE_TELEGRAM_CHANNEL_DISCONNECTED, description) + write_chatops_insight_log( + author=user, + event_name=ChatOpsEvent.CHANNEL_DISCONNECTED, + chatops_type=ChatOpsType.TELEGRAM, + channel_name=instance.channel_name, + ) instance.delete() diff --git a/engine/apps/api/views/user.py b/engine/apps/api/views/user.py index 14a8b43e..ed3f633b 100644 --- a/engine/apps/api/views/user.py +++ b/engine/apps/api/views/user.py @@ -40,12 +40,18 @@ from apps.telegram.models import TelegramVerificationCode from apps.twilioapp.phone_manager import PhoneManager from apps.twilioapp.twilio_client import twilio_client from apps.user_management.models import User -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.exceptions import Conflict from common.api_helpers.mixins import FilterSerializerMixin, PublicPrimaryKeyMixin from common.api_helpers.paginators import HundredPageSizePaginator from common.api_helpers.utils import create_engine_url from common.constants.role import Role +from common.insight_log import ( + ChatOpsEvent, + ChatOpsType, + EntityEvent, + write_chatops_insight_log, + write_resource_insight_log, +) logger = logging.getLogger(__name__) @@ -259,41 +265,37 @@ class UserView( def verify_number(self, request, pk): target_user = self.get_object() code = request.query_params.get("token", None) - old_state = target_user.repr_settings_for_client_side_logging + prev_state = target_user.insight_logs_serialized phone_manager = PhoneManager(target_user) verified, error = phone_manager.verify_phone_number(code) if not verified: return Response(error, status=status.HTTP_400_BAD_REQUEST) - organization = request.auth.organization - new_state = target_user.repr_settings_for_client_side_logging - description = f"User settings for user {target_user.username} was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log( - organization, - request.user, - OrganizationLogType.TYPE_USER_SETTINGS_CHANGED, - description, + new_state = target_user.insight_logs_serialized + write_resource_insight_log( + instance=target_user, + author=self.request.user, + event=EntityEvent.UPDATED, + prev_state=prev_state, + new_state=new_state, ) return Response(status=status.HTTP_200_OK) @action(detail=True, methods=["put"]) def forget_number(self, request, pk): target_user = self.get_object() - old_state = target_user.repr_settings_for_client_side_logging + prev_state = target_user.insight_logs_serialized phone_manager = PhoneManager(target_user) forget = phone_manager.forget_phone_number() if forget: - organization = request.auth.organization - new_state = target_user.repr_settings_for_client_side_logging - description = ( - f"User settings for user {target_user.username} was changed from:\n{old_state}\nto:\n{new_state}" - ) - create_organization_log( - organization, - request.user, - OrganizationLogType.TYPE_USER_SETTINGS_CHANGED, - description, + new_state = target_user.insight_logs_serialized + write_resource_insight_log( + instance=target_user, + author=self.request.user, + event=EntityEvent.UPDATED, + prev_state=prev_state, + new_state=new_state, ) return Response(status=status.HTTP_200_OK) @@ -352,25 +354,23 @@ class UserView( def unlink_telegram(self, request, pk): user = self.get_object() TelegramToUserConnector = apps.get_model("telegram", "TelegramToUserConnector") - try: connector = TelegramToUserConnector.objects.get(user=user) connector.delete() + write_chatops_insight_log( + author=request.user, + event_name=ChatOpsEvent.USER_UNLINKED, + chatops_type=ChatOpsType.TELEGRAM, + user=user.username, + user_id=user.public_primary_key, + ) except TelegramToUserConnector.DoesNotExist: return Response(status=status.HTTP_400_BAD_REQUEST) - - description = f"Telegram account of user {user.username} was disconnected" - create_organization_log( - user.organization, - user, - OrganizationLogType.TYPE_TELEGRAM_FROM_USER_DISCONNECTED, - description, - ) - return Response(status=status.HTTP_200_OK) @action(detail=True, methods=["post"]) def unlink_backend(self, request, pk): + # TODO: insight logs support backend_id = request.query_params.get("backend") backend = get_messaging_backend_from_id(backend_id) if backend is None: @@ -379,17 +379,15 @@ class UserView( user = self.get_object() try: backend.unlink_user(user) + write_chatops_insight_log( + author=request.user, + event_name=ChatOpsEvent.USER_UNLINKED, + chatops_type=backend.backend_id, + user=user.username, + user_id=user.public_primary_key, + ) except ObjectDoesNotExist: return Response(status=status.HTTP_400_BAD_REQUEST) - - description = f"{backend.label} account of user {user.username} was disconnected" - create_organization_log( - user.organization, - user, - OrganizationLogType.TYPE_MESSAGING_BACKEND_USER_DISCONNECTED, - description, - ) - return Response(status=status.HTTP_200_OK) @action(detail=True, methods=["get", "post", "delete"]) @@ -412,6 +410,7 @@ class UserView( if self.request.method == "POST": try: instance, token = UserScheduleExportAuthToken.create_auth_token(user, user.organization) + write_resource_insight_log(instance=instance, author=self.request.user, event=EntityEvent.CREATED) except IntegrityError: raise Conflict("Schedule export token for user already exists") @@ -426,10 +425,10 @@ class UserView( if self.request.method == "DELETE": try: token = UserScheduleExportAuthToken.objects.get(user=user) + write_resource_insight_log(instance=token, author=self.request.user, event=EntityEvent.DELETED) token.delete() except UserScheduleExportAuthToken.DoesNotExist: raise NotFound - return Response(status=status.HTTP_204_NO_CONTENT) @action(detail=True, methods=["get", "post", "delete"]) diff --git a/engine/apps/api/views/user_notification_policy.py b/engine/apps/api/views/user_notification_policy.py index c36f5106..ae7e4bee 100644 --- a/engine/apps/api/views/user_notification_policy.py +++ b/engine/apps/api/views/user_notification_policy.py @@ -24,9 +24,9 @@ from apps.base.messaging import get_messaging_backend_from_id from apps.base.models import UserNotificationPolicy from apps.base.models.user_notification_policy import BUILT_IN_BACKENDS, NotificationChannelAPIOptions from apps.user_management.models import User -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.exceptions import BadRequest from common.api_helpers.mixins import UpdateSerializerMixin +from common.insight_log import EntityEvent, write_resource_insight_log class UserNotificationPolicyView(UpdateSerializerMixin, ModelViewSet): @@ -83,45 +83,42 @@ class UserNotificationPolicyView(UpdateSerializerMixin, ModelViewSet): return obj def perform_create(self, serializer): - organization = self.request.auth.organization user = serializer.validated_data.get("user") or self.request.user - old_state = user.repr_settings_for_client_side_logging + prev_state = user.insight_logs_serialized serializer.save() - new_state = user.repr_settings_for_client_side_logging - description = f"User settings for user {user.username} was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log( - organization, - self.request.user, - OrganizationLogType.TYPE_USER_SETTINGS_CHANGED, - description, + new_state = user.insight_logs_serialized + write_resource_insight_log( + instance=user, + author=self.request.user, + event=EntityEvent.UPDATED, + prev_state=prev_state, + new_state=new_state, ) def perform_update(self, serializer): - organization = self.request.auth.organization user = serializer.validated_data.get("user") or self.request.user - old_state = user.repr_settings_for_client_side_logging + prev_state = user.insight_logs_serialized serializer.save() - new_state = user.repr_settings_for_client_side_logging - description = f"User settings for user {user.username} was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log( - organization, - self.request.user, - OrganizationLogType.TYPE_USER_SETTINGS_CHANGED, - description, + new_state = user.insight_logs_serialized + write_resource_insight_log( + instance=user, + author=self.request.user, + event=EntityEvent.UPDATED, + prev_state=prev_state, + new_state=new_state, ) def perform_destroy(self, instance): - organization = self.request.auth.organization user = instance.user - old_state = user.repr_settings_for_client_side_logging + prev_state = user.insight_logs_serialized instance.delete() - new_state = user.repr_settings_for_client_side_logging - description = f"User settings for user {user.username} was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log( - organization, - self.request.user, - OrganizationLogType.TYPE_USER_SETTINGS_CHANGED, - description, + new_state = user.insight_logs_serialized + write_resource_insight_log( + instance=user, + author=self.request.user, + event=EntityEvent.UPDATED, + prev_state=prev_state, + new_state=new_state, ) @action(detail=True, methods=["put"]) diff --git a/engine/apps/auth_token/models/api_auth_token.py b/engine/apps/auth_token/models/api_auth_token.py index 3309a505..531c00be 100644 --- a/engine/apps/auth_token/models/api_auth_token.py +++ b/engine/apps/auth_token/models/api_auth_token.py @@ -5,7 +5,6 @@ from django.db import models from apps.auth_token import constants, crypto from apps.auth_token.models.base_auth_token import BaseAuthToken from apps.user_management.models import Organization, User -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log class ApiAuthToken(BaseAuthToken): @@ -27,6 +26,22 @@ class ApiAuthToken(BaseAuthToken): organization=organization, name=name, ) - description = f"API token {instance.name} was created" - create_organization_log(organization, user, OrganizationLogType.TYPE_API_TOKEN_CREATED, description) return instance, token_string + + # Insight logs + @property + def insight_logs_type_verbal(self): + return "public_api_token" + + @property + def insight_logs_verbal(self): + return self.name + + @property + def insight_logs_serialized(self): + # API tokens are not modifiable, so return empty dict to implement InsightLoggable interface + return {} + + @property + def insight_logs_metadata(self): + return {} diff --git a/engine/apps/auth_token/models/schedule_export_auth_token.py b/engine/apps/auth_token/models/schedule_export_auth_token.py index 4df656f9..e141b39f 100644 --- a/engine/apps/auth_token/models/schedule_export_auth_token.py +++ b/engine/apps/auth_token/models/schedule_export_auth_token.py @@ -6,7 +6,6 @@ from apps.auth_token import constants, crypto from apps.auth_token.models.base_auth_token import BaseAuthToken from apps.schedules.models import OnCallSchedule from apps.user_management.models import Organization, User -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log class ScheduleExportAuthToken(BaseAuthToken): @@ -38,8 +37,22 @@ class ScheduleExportAuthToken(BaseAuthToken): organization=organization, schedule=schedule, ) - description = "Schedule export token was created by user {0} for schedule {1}".format( - user.username, schedule.name - ) - create_organization_log(organization, user, OrganizationLogType.TYPE_SCHEDULE_EXPORT_TOKEN_CREATED, description) return instance, token_string + + # Insight logs + @property + def insight_logs_type_verbal(self): + return "schedule_export_token" + + @property + def insight_logs_verbal(self): + return f"Schedule export token for {self.schedule.insight_logs_verbal}" + + @property + def insight_logs_serialized(self): + # Schedule export tokens are not modifiable, return empty dict to implement InsightLoggable interface + return {} + + @property + def insight_logs_metadata(self): + return {} diff --git a/engine/apps/auth_token/models/user_schedule_export_auth_token.py b/engine/apps/auth_token/models/user_schedule_export_auth_token.py index 34242dab..94186408 100644 --- a/engine/apps/auth_token/models/user_schedule_export_auth_token.py +++ b/engine/apps/auth_token/models/user_schedule_export_auth_token.py @@ -5,7 +5,6 @@ from django.db import models from apps.auth_token import constants, crypto from apps.auth_token.models.base_auth_token import BaseAuthToken from apps.user_management.models import Organization, User -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log class UserScheduleExportAuthToken(BaseAuthToken): @@ -31,6 +30,22 @@ class UserScheduleExportAuthToken(BaseAuthToken): user=user, organization=organization, ) - description = "User schedule export token was created by user {0}".format(user.username) - create_organization_log(organization, user, OrganizationLogType.TYPE_SCHEDULE_EXPORT_TOKEN_CREATED, description) return instance, token_string + + # Insight logs + @property + def insight_logs_type_verbal(self): + return "user_schedule_export_token" + + @property + def insight_logs_verbal(self): + return f"Users chedule export token for {self.user.username}" + + @property + def insight_logs_serialized(self): + # Schedule export tokens are not modifiable, return empty dict to implement InsightLoggable interface + return {} + + @property + def insight_logs_metadata(self): + return {} diff --git a/engine/apps/base/migrations/0001_squashed_initial.py b/engine/apps/base/migrations/0001_squashed_initial.py index dfdf66d3..0055fa88 100644 --- a/engine/apps/base/migrations/0001_squashed_initial.py +++ b/engine/apps/base/migrations/0001_squashed_initial.py @@ -1,7 +1,6 @@ # Generated by Django 3.2.5 on 2022-05-31 14:46 import apps.base.models.live_setting -import apps.base.models.organization_log_record import apps.base.models.user_notification_policy import datetime import django.core.validators @@ -51,7 +50,7 @@ class Migration(migrations.Migration): name='OrganizationLogRecord', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('public_primary_key', models.CharField(default=apps.base.models.organization_log_record.generate_public_primary_key_for_organization_log, max_length=20, unique=True, validators=[django.core.validators.MinLengthValidator(13)])), + ('public_primary_key', models.CharField(max_length=20, null=True, default=None)), ('created_at', models.DateTimeField(auto_now_add=True)), ('description', models.TextField(default=None, null=True)), ('_labels', models.JSONField(default=list)), diff --git a/engine/apps/base/migrations/0003_delete_organizationlogrecord.py b/engine/apps/base/migrations/0003_delete_organizationlogrecord.py new file mode 100644 index 00000000..b0a49a1a --- /dev/null +++ b/engine/apps/base/migrations/0003_delete_organizationlogrecord.py @@ -0,0 +1,16 @@ +# Generated by Django 3.2.5 on 2022-08-23 12:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0002_squashed_initial'), + ] + + operations = [ + migrations.DeleteModel( + name='OrganizationLogRecord', + ), + ] diff --git a/engine/apps/base/models/__init__.py b/engine/apps/base/models/__init__.py index 7dc0d486..336ee36e 100644 --- a/engine/apps/base/models/__init__.py +++ b/engine/apps/base/models/__init__.py @@ -1,6 +1,5 @@ from .dynamic_setting import DynamicSetting # noqa: F401 from .failed_to_invoke_celery_task import FailedToInvokeCeleryTask # noqa: F401 from .live_setting import LiveSetting # noqa: F401 -from .organization_log_record import OrganizationLogRecord # noqa: F401 from .user_notification_policy import UserNotificationPolicy # noqa: F401 from .user_notification_policy_log_record import UserNotificationPolicyLogRecord # noqa: F401 diff --git a/engine/apps/base/models/organization_log_record.py b/engine/apps/base/models/organization_log_record.py deleted file mode 100644 index 9f4e06b3..00000000 --- a/engine/apps/base/models/organization_log_record.py +++ /dev/null @@ -1,317 +0,0 @@ -from django.apps import apps -from django.conf import settings -from django.core.validators import MinLengthValidator -from django.db import models -from django.db.models import JSONField -from emoji import emojize - -from apps.alerts.models.maintainable_object import MaintainableObject -from apps.user_management.organization_log_creator import OrganizationLogType -from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length - - -def generate_public_primary_key_for_organization_log(): - prefix = "V" - new_public_primary_key = generate_public_primary_key(prefix) - - failure_counter = 0 - while OrganizationLogRecord.objects.filter(public_primary_key=new_public_primary_key).exists(): - new_public_primary_key = increase_public_primary_key_length( - failure_counter=failure_counter, prefix=prefix, model_name="OrganizationLogRecord" - ) - failure_counter += 1 - - return new_public_primary_key - - -class OrganizationLogRecordManager(models.Manager): - def create(self, organization, author, type, description): - # set labels - labels = OrganizationLogRecord.LABELS_FOR_TYPE[type] - return super().create( - organization=organization, - author=author, - description=description, - _labels=labels, - ) - - -class OrganizationLogRecord(models.Model): - - objects = OrganizationLogRecordManager() - - LABEL_ORGANIZATION = "organization" - LABEL_SLACK = "slack" - LABEL_TELEGRAM = "telegram" - LABEL_DEFAULT_CHANNEL = "default channel" - LABEL_SLACK_WORKSPACE_CONNECTED = "slack workspace connected" - LABEL_SLACK_WORKSPACE_DISCONNECTED = "slack workspace disconnected" - LABEL_TELEGRAM_CHANNEL_CONNECTED = "telegram channel connected" - LABEL_TELEGRAM_CHANNEL_DISCONNECTED = "telegram channel disconnected" - LABEL_INTEGRATION = "integration" - LABEL_INTEGRATION_CREATED = "integration created" - LABEL_INTEGRATION_DELETED = "integration deleted" - LABEL_INTEGRATION_CHANGED = "integration changed" - LABEL_INTEGRATION_HEARTBEAT = "integration heartbeat" - LABEL_INTEGRATION_HEARTBEAT_CREATED = "integration heartbeat created" - LABEL_INTEGRATION_HEARTBEAT_CHANGED = "integration heartbeat changed" - LABEL_MAINTENANCE = "maintenance" - LABEL_MAINTENANCE_STARTED = "maintenance started" - LABEL_MAINTENANCE_STOPPED = "maintenance stopped" - LABEL_DEBUG = "debug" - LABEL_DEBUG_STARTED = "debug started" - LABEL_DEBUG_STOPPED = "debug stopped" - LABEL_CHANNEL_FILTER = "route" - LABEL_CHANNEL_FILTER_CREATED = "route created" - LABEL_CHANNEL_FILTER_CHANGED = "route changed" - LABEL_CHANNEL_FILTER_DELETED = "route deleted" - LABEL_ESCALATION_CHAIN = "escalation chain" - LABEL_ESCALATION_CHAIN_CREATED = "escalation chain created" - LABEL_ESCALATION_CHAIN_DELETED = "escalation chain deleted" - LABEL_ESCALATION_CHAIN_CHANGED = "escalation chain changed" - LABEL_ESCALATION_POLICY = "escalation policy" - LABEL_ESCALATION_POLICY_CREATED = "escalation policy created" - LABEL_ESCALATION_POLICY_DELETED = "escalation policy deleted" - LABEL_ESCALATION_POLICY_CHANGED = "escalation policy changed" - LABEL_CUSTOM_ACTION = "custom action" - LABEL_CUSTOM_ACTION_CREATED = "custom action created" - LABEL_CUSTOM_ACTION_DELETED = "custom action deleted" - LABEL_CUSTOM_ACTION_CHANGED = "custom action changed" - LABEL_SCHEDULE = "schedule" - LABEL_SCHEDULE_CREATED = "schedule created" - LABEL_SCHEDULE_DELETED = "schedule deleted" - LABEL_SCHEDULE_CHANGED = "schedule changed" - LABEL_ON_CALL_SHIFT = "on-call shift" - LABEL_ON_CALL_SHIFT_CREATED = "on-call shift created" - LABEL_ON_CALL_SHIFT_DELETED = "on-call shift deleted" - LABEL_ON_CALL_SHIFT_CHANGED = "on-call shift changed" - LABEL_USER = "user" - LABEL_USER_CREATED = "user created" - LABEL_USER_SETTINGS_CHANGED = "user changed" - LABEL_ORGANIZATION_SETTINGS_CHANGED = "organization settings changed" - LABEL_TELEGRAM_TO_USER_CONNECTED = "telegram to user connected" - LABEL_TELEGRAM_FROM_USER_DISCONNECTED = "telegram from user disconnected" - LABEL_API_TOKEN = "api token" - LABEL_API_TOKEN_CREATED = "api token created" - LABEL_API_TOKEN_REVOKED = "api token revoked" - LABEL_ESCALATION_CHAIN_COPIED = "escalation chain copied" - LABEL_SCHEDULE_EXPORT_TOKEN = "schedule export token" - LABEL_SCHEDULE_EXPORT_TOKEN_CREATED = "schedule export token created" - LABEL_MESSAGING_BACKEND_CHANNEL_CHANGED = "messaging backend channel changed" - LABEL_MESSAGING_BACKEND_CHANNEL_DELETED = "messaging backend channel deleted" - LABEL_MESSAGING_BACKEND_USER_DISCONNECTED = "messaging backend user disconnected" - - LABELS = [ - LABEL_ORGANIZATION, - LABEL_SLACK, - LABEL_TELEGRAM, - LABEL_DEFAULT_CHANNEL, - LABEL_SLACK_WORKSPACE_CONNECTED, - LABEL_SLACK_WORKSPACE_DISCONNECTED, - LABEL_TELEGRAM_CHANNEL_CONNECTED, - LABEL_TELEGRAM_CHANNEL_DISCONNECTED, - LABEL_INTEGRATION, - LABEL_INTEGRATION_CREATED, - LABEL_INTEGRATION_DELETED, - LABEL_INTEGRATION_CHANGED, - LABEL_INTEGRATION_HEARTBEAT, - LABEL_INTEGRATION_HEARTBEAT_CREATED, - LABEL_INTEGRATION_HEARTBEAT_CHANGED, - LABEL_MAINTENANCE, - LABEL_MAINTENANCE_STARTED, - LABEL_MAINTENANCE_STOPPED, - LABEL_DEBUG, - LABEL_DEBUG_STARTED, - LABEL_DEBUG_STOPPED, - LABEL_CHANNEL_FILTER, - LABEL_CHANNEL_FILTER_CREATED, - LABEL_CHANNEL_FILTER_CHANGED, - LABEL_CHANNEL_FILTER_DELETED, - LABEL_ESCALATION_CHAIN, - LABEL_ESCALATION_CHAIN_CREATED, - LABEL_ESCALATION_CHAIN_DELETED, - LABEL_ESCALATION_CHAIN_CHANGED, - LABEL_ESCALATION_POLICY, - LABEL_ESCALATION_POLICY_CREATED, - LABEL_ESCALATION_POLICY_DELETED, - LABEL_ESCALATION_POLICY_CHANGED, - LABEL_CUSTOM_ACTION, - LABEL_CUSTOM_ACTION_CREATED, - LABEL_CUSTOM_ACTION_DELETED, - LABEL_CUSTOM_ACTION_CHANGED, - LABEL_SCHEDULE, - LABEL_SCHEDULE_CREATED, - LABEL_SCHEDULE_DELETED, - LABEL_SCHEDULE_CHANGED, - LABEL_ON_CALL_SHIFT, - LABEL_ON_CALL_SHIFT_CREATED, - LABEL_ON_CALL_SHIFT_DELETED, - LABEL_ON_CALL_SHIFT_CHANGED, - LABEL_USER, - LABEL_USER_CREATED, - LABEL_USER_SETTINGS_CHANGED, - LABEL_ORGANIZATION_SETTINGS_CHANGED, - LABEL_TELEGRAM_TO_USER_CONNECTED, - LABEL_TELEGRAM_FROM_USER_DISCONNECTED, - LABEL_API_TOKEN, - LABEL_API_TOKEN_CREATED, - LABEL_API_TOKEN_REVOKED, - LABEL_ESCALATION_CHAIN_COPIED, - LABEL_SCHEDULE_EXPORT_TOKEN, - LABEL_MESSAGING_BACKEND_CHANNEL_CHANGED, - LABEL_MESSAGING_BACKEND_CHANNEL_DELETED, - LABEL_MESSAGING_BACKEND_USER_DISCONNECTED, - ] - - LABELS_FOR_TYPE = { - OrganizationLogType.TYPE_SLACK_DEFAULT_CHANNEL_CHANGED: [LABEL_SLACK, LABEL_DEFAULT_CHANNEL], - OrganizationLogType.TYPE_SLACK_WORKSPACE_CONNECTED: [LABEL_SLACK, LABEL_SLACK_WORKSPACE_CONNECTED], - OrganizationLogType.TYPE_SLACK_WORKSPACE_DISCONNECTED: [LABEL_SLACK, LABEL_SLACK_WORKSPACE_DISCONNECTED], - OrganizationLogType.TYPE_TELEGRAM_DEFAULT_CHANNEL_CHANGED: [LABEL_TELEGRAM, LABEL_DEFAULT_CHANNEL], - OrganizationLogType.TYPE_TELEGRAM_CHANNEL_CONNECTED: [LABEL_TELEGRAM, LABEL_TELEGRAM_CHANNEL_CONNECTED], - OrganizationLogType.TYPE_TELEGRAM_CHANNEL_DISCONNECTED: [LABEL_TELEGRAM, LABEL_TELEGRAM_CHANNEL_DISCONNECTED], - OrganizationLogType.TYPE_INTEGRATION_CREATED: [LABEL_INTEGRATION, LABEL_INTEGRATION_CREATED], - OrganizationLogType.TYPE_INTEGRATION_DELETED: [LABEL_INTEGRATION, LABEL_INTEGRATION_DELETED], - OrganizationLogType.TYPE_INTEGRATION_CHANGED: [LABEL_INTEGRATION, LABEL_INTEGRATION_CHANGED], - OrganizationLogType.TYPE_HEARTBEAT_CREATED: [LABEL_INTEGRATION_HEARTBEAT, LABEL_INTEGRATION_HEARTBEAT_CREATED], - OrganizationLogType.TYPE_HEARTBEAT_CHANGED: [LABEL_INTEGRATION_HEARTBEAT, LABEL_INTEGRATION_HEARTBEAT_CHANGED], - OrganizationLogType.TYPE_CHANNEL_FILTER_CREATED: [LABEL_CHANNEL_FILTER, LABEL_CHANNEL_FILTER_CREATED], - OrganizationLogType.TYPE_CHANNEL_FILTER_DELETED: [LABEL_CHANNEL_FILTER, LABEL_CHANNEL_FILTER_DELETED], - OrganizationLogType.TYPE_CHANNEL_FILTER_CHANGED: [LABEL_CHANNEL_FILTER, LABEL_CHANNEL_FILTER_CHANGED], - OrganizationLogType.TYPE_ESCALATION_CHAIN_CREATED: [LABEL_ESCALATION_CHAIN, LABEL_ESCALATION_CHAIN_CREATED], - OrganizationLogType.TYPE_ESCALATION_CHAIN_DELETED: [LABEL_ESCALATION_CHAIN, LABEL_ESCALATION_CHAIN_DELETED], - OrganizationLogType.TYPE_ESCALATION_CHAIN_CHANGED: [LABEL_ESCALATION_CHAIN, LABEL_ESCALATION_CHAIN_CHANGED], - OrganizationLogType.TYPE_ESCALATION_STEP_CREATED: [LABEL_ESCALATION_POLICY, LABEL_ESCALATION_POLICY_CREATED], - OrganizationLogType.TYPE_ESCALATION_STEP_DELETED: [LABEL_ESCALATION_POLICY, LABEL_ESCALATION_POLICY_DELETED], - OrganizationLogType.TYPE_ESCALATION_STEP_CHANGED: [LABEL_ESCALATION_POLICY, LABEL_ESCALATION_POLICY_CHANGED], - OrganizationLogType.TYPE_MAINTENANCE_STARTED_FOR_ORGANIZATION: [ - LABEL_MAINTENANCE, - LABEL_MAINTENANCE_STARTED, - LABEL_ORGANIZATION, - ], - OrganizationLogType.TYPE_MAINTENANCE_STARTED_FOR_INTEGRATION: [ - LABEL_MAINTENANCE, - LABEL_MAINTENANCE_STARTED, - LABEL_INTEGRATION, - ], - OrganizationLogType.TYPE_MAINTENANCE_STOPPED_FOR_ORGANIZATION: [ - LABEL_MAINTENANCE, - LABEL_MAINTENANCE_STOPPED, - LABEL_ORGANIZATION, - ], - OrganizationLogType.TYPE_MAINTENANCE_STOPPED_FOR_INTEGRATION: [ - LABEL_MAINTENANCE, - LABEL_MAINTENANCE_STOPPED, - LABEL_INTEGRATION, - ], - OrganizationLogType.TYPE_MAINTENANCE_DEBUG_STARTED_FOR_ORGANIZATION: [ - LABEL_DEBUG, - LABEL_DEBUG_STARTED, - LABEL_ORGANIZATION, - ], - OrganizationLogType.TYPE_MAINTENANCE_DEBUG_STARTED_FOR_INTEGRATION: [ - LABEL_DEBUG, - LABEL_DEBUG_STARTED, - LABEL_INTEGRATION, - ], - OrganizationLogType.TYPE_MAINTENANCE_DEBUG_STOPPED_FOR_ORGANIZATION: [ - LABEL_DEBUG, - LABEL_DEBUG_STOPPED, - LABEL_ORGANIZATION, - ], - OrganizationLogType.TYPE_MAINTENANCE_DEBUG_STOPPED_FOR_INTEGRATION: [ - LABEL_DEBUG, - LABEL_DEBUG_STOPPED, - LABEL_INTEGRATION, - ], - OrganizationLogType.TYPE_CUSTOM_ACTION_CREATED: [LABEL_CUSTOM_ACTION, LABEL_CUSTOM_ACTION_CREATED], - OrganizationLogType.TYPE_CUSTOM_ACTION_DELETED: [LABEL_CUSTOM_ACTION, LABEL_CUSTOM_ACTION_DELETED], - OrganizationLogType.TYPE_CUSTOM_ACTION_CHANGED: [LABEL_CUSTOM_ACTION, LABEL_CUSTOM_ACTION_CHANGED], - OrganizationLogType.TYPE_SCHEDULE_CREATED: [LABEL_SCHEDULE, LABEL_SCHEDULE_CREATED], - OrganizationLogType.TYPE_SCHEDULE_DELETED: [LABEL_SCHEDULE, LABEL_SCHEDULE_DELETED], - OrganizationLogType.TYPE_SCHEDULE_CHANGED: [LABEL_SCHEDULE, LABEL_SCHEDULE_CHANGED], - OrganizationLogType.TYPE_ON_CALL_SHIFT_CREATED: [LABEL_ON_CALL_SHIFT, LABEL_ON_CALL_SHIFT_CREATED], - OrganizationLogType.TYPE_ON_CALL_SHIFT_DELETED: [LABEL_ON_CALL_SHIFT, LABEL_ON_CALL_SHIFT_DELETED], - OrganizationLogType.TYPE_ON_CALL_SHIFT_CHANGED: [LABEL_ON_CALL_SHIFT, LABEL_ON_CALL_SHIFT_CHANGED], - OrganizationLogType.TYPE_NEW_USER_ADDED: [LABEL_USER, LABEL_USER_CREATED], - OrganizationLogType.TYPE_ORGANIZATION_SETTINGS_CHANGED: [ - LABEL_ORGANIZATION, - LABEL_ORGANIZATION_SETTINGS_CHANGED, - ], - OrganizationLogType.TYPE_USER_SETTINGS_CHANGED: [LABEL_USER, LABEL_USER_SETTINGS_CHANGED], - OrganizationLogType.TYPE_TELEGRAM_TO_USER_CONNECTED: [LABEL_TELEGRAM, LABEL_TELEGRAM_TO_USER_CONNECTED], - OrganizationLogType.TYPE_TELEGRAM_FROM_USER_DISCONNECTED: [ - LABEL_TELEGRAM, - LABEL_TELEGRAM_FROM_USER_DISCONNECTED, - ], - OrganizationLogType.TYPE_API_TOKEN_CREATED: [LABEL_API_TOKEN, LABEL_API_TOKEN_CREATED], - OrganizationLogType.TYPE_API_TOKEN_REVOKED: [LABEL_API_TOKEN, LABEL_API_TOKEN_REVOKED], - OrganizationLogType.TYPE_ESCALATION_CHAIN_COPIED: [LABEL_ESCALATION_CHAIN, LABEL_ESCALATION_CHAIN_COPIED], - OrganizationLogType.TYPE_SCHEDULE_EXPORT_TOKEN_CREATED: [ - LABEL_SCHEDULE_EXPORT_TOKEN, - LABEL_SCHEDULE_EXPORT_TOKEN_CREATED, - ], - OrganizationLogType.TYPE_MESSAGING_BACKEND_CHANNEL_CHANGED: [LABEL_MESSAGING_BACKEND_CHANNEL_CHANGED], - OrganizationLogType.TYPE_MESSAGING_BACKEND_CHANNEL_DELETED: [LABEL_MESSAGING_BACKEND_CHANNEL_DELETED], - OrganizationLogType.TYPE_MESSAGING_BACKEND_USER_DISCONNECTED: [LABEL_MESSAGING_BACKEND_USER_DISCONNECTED], - } - - public_primary_key = models.CharField( - max_length=20, - validators=[MinLengthValidator(settings.PUBLIC_PRIMARY_KEY_MIN_LENGTH + 1)], - unique=True, - default=generate_public_primary_key_for_organization_log, - ) - - organization = models.ForeignKey( - "user_management.Organization", on_delete=models.CASCADE, related_name="log_records" - ) - author = models.ForeignKey( - "user_management.User", - on_delete=models.SET_NULL, - related_name="team_log_records", - default=None, - null=True, - ) - - created_at = models.DateTimeField(auto_now_add=True) - description = models.TextField(null=True, default=None) - _labels = JSONField(default=list) - - @property - def labels(self): - return self._labels - - @staticmethod - def get_log_type_and_maintainable_object_verbal(maintainable_obj, mode, verbal, stopped=False): - AlertReceiveChannel = apps.get_model("alerts", "AlertReceiveChannel") - Organization = apps.get_model("user_management", "Organization") - object_verbal_map = { - AlertReceiveChannel: f"integration {emojize(verbal, use_aliases=True)}", - Organization: "organization", - } - if stopped: - log_type_map = { - AlertReceiveChannel: { - MaintainableObject.DEBUG_MAINTENANCE: OrganizationLogType.TYPE_MAINTENANCE_DEBUG_STOPPED_FOR_INTEGRATION, - MaintainableObject.MAINTENANCE: OrganizationLogType.TYPE_MAINTENANCE_STOPPED_FOR_INTEGRATION, - }, - Organization: { - MaintainableObject.DEBUG_MAINTENANCE: OrganizationLogType.TYPE_MAINTENANCE_DEBUG_STOPPED_FOR_ORGANIZATION, - MaintainableObject.MAINTENANCE: OrganizationLogType.TYPE_MAINTENANCE_STOPPED_FOR_ORGANIZATION, - }, - } - else: - log_type_map = { - AlertReceiveChannel: { - MaintainableObject.DEBUG_MAINTENANCE: OrganizationLogType.TYPE_MAINTENANCE_DEBUG_STARTED_FOR_INTEGRATION, - MaintainableObject.MAINTENANCE: OrganizationLogType.TYPE_MAINTENANCE_STARTED_FOR_INTEGRATION, - }, - Organization: { - MaintainableObject.DEBUG_MAINTENANCE: OrganizationLogType.TYPE_MAINTENANCE_DEBUG_STARTED_FOR_ORGANIZATION, - MaintainableObject.MAINTENANCE: OrganizationLogType.TYPE_MAINTENANCE_STARTED_FOR_ORGANIZATION, - }, - } - log_type = log_type_map[type(maintainable_obj)][mode] - object_verbal = object_verbal_map[type(maintainable_obj)] - return log_type, object_verbal diff --git a/engine/apps/base/models/user_notification_policy.py b/engine/apps/base/models/user_notification_policy.py index 2e76824a..e0a62275 100644 --- a/engine/apps/base/models/user_notification_policy.py +++ b/engine/apps/base/models/user_notification_policy.py @@ -11,7 +11,6 @@ from ordered_model.models import OrderedModel from apps.base.messaging import get_messaging_backends from apps.user_management.models import User -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length @@ -81,20 +80,11 @@ class UserNotificationPolicyQuerySet(models.QuerySet): if notification_policies.exists(): return notification_policies - old_state = user.repr_settings_for_client_side_logging if important: policies = self.create_important_policies_for_user(user) else: policies = self.create_default_policies_for_user(user) - new_state = user.repr_settings_for_client_side_logging - description = f"User settings for user {user.username} was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log( - user.organization, - None, - OrganizationLogType.TYPE_USER_SETTINGS_CHANGED, - description, - ) return policies def create_default_policies_for_user(self, user: User) -> "QuerySet[UserNotificationPolicy]": diff --git a/engine/apps/base/tests/factories.py b/engine/apps/base/tests/factories.py index d65496fb..6d1a29d3 100644 --- a/engine/apps/base/tests/factories.py +++ b/engine/apps/base/tests/factories.py @@ -1,6 +1,6 @@ import factory -from apps.base.models import LiveSetting, OrganizationLogRecord, UserNotificationPolicy, UserNotificationPolicyLogRecord +from apps.base.models import LiveSetting, UserNotificationPolicy, UserNotificationPolicyLogRecord class UserNotificationPolicyFactory(factory.DjangoModelFactory): @@ -13,13 +13,6 @@ class UserNotificationPolicyLogRecordFactory(factory.DjangoModelFactory): model = UserNotificationPolicyLogRecord -class OrganizationLogRecordFactory(factory.DjangoModelFactory): - description = factory.Faker("sentence", nb_words=4) - - class Meta: - model = OrganizationLogRecord - - class LiveSettingFactory(factory.DjangoModelFactory): class Meta: model = LiveSetting diff --git a/engine/apps/base/tests/test_organization_log_record.py b/engine/apps/base/tests/test_organization_log_record.py deleted file mode 100644 index 26d7c7b0..00000000 --- a/engine/apps/base/tests/test_organization_log_record.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest - -from apps.base.models import OrganizationLogRecord - - -@pytest.mark.django_db -def test_organization_log_set_general_log_channel( - make_organization_with_slack_team_identity, make_user_for_organization, make_slack_channel -): - organization, slack_team_identity = make_organization_with_slack_team_identity() - user = make_user_for_organization(organization) - - slack_channel = make_slack_channel(slack_team_identity) - organization.set_general_log_channel(slack_channel.slack_id, slack_channel.name, user) - - assert organization.log_records.filter( - _labels=[OrganizationLogRecord.LABEL_SLACK, OrganizationLogRecord.LABEL_DEFAULT_CHANNEL] - ).exists() diff --git a/engine/apps/heartbeat/models.py b/engine/apps/heartbeat/models.py index 034c3f1d..4f9019dc 100644 --- a/engine/apps/heartbeat/models.py +++ b/engine/apps/heartbeat/models.py @@ -1,7 +1,6 @@ import logging from urllib.parse import urljoin -import humanize from django.conf import settings from django.core.validators import MinLengthValidator from django.db import models, transaction @@ -171,14 +170,6 @@ class IntegrationHeartBeat(BaseHeartBeat): "alerts.AlertReceiveChannel", on_delete=models.CASCADE, related_name="integration_heartbeat" ) - @property - def repr_settings_for_client_side_logging(self): - """ - Example of execution: - timeout: 30 minutes - """ - return f"timeout: {humanize.naturaldelta(self.timeout_seconds)}" - @property def is_expired(self): if self.last_heartbeat_time is not None: @@ -242,3 +233,25 @@ class IntegrationHeartBeat(BaseHeartBeat): (43200, "12 hours"), (86400, "1 day"), ) + + # Insight logs + @property + def insight_logs_type_verbal(self): + return "integration_heartbeat" + + @property + def insight_logs_verbal(self): + return f"Integration Heartbeat for {self.alert_receive_channel.insight_logs_verbal}" + + @property + def insight_logs_serialized(self): + return { + "timeout": self.timeout_seconds, + } + + @property + def insight_logs_metadata(self): + return { + "integration": self.alert_receive_channel.insight_logs_verbal, + "integration_id": self.alert_receive_channel.public_primary_key, + } diff --git a/engine/apps/public_api/views/action.py b/engine/apps/public_api/views/action.py index 0e5944eb..1d1ecdb8 100644 --- a/engine/apps/public_api/views/action.py +++ b/engine/apps/public_api/views/action.py @@ -6,10 +6,10 @@ from apps.alerts.models import CustomButton from apps.auth_token.auth import ApiTokenAuthentication from apps.public_api.serializers.action import ActionCreateSerializer, ActionUpdateSerializer from apps.public_api.throttlers.user_throttle import UserThrottle -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.filters import ByTeamFilter from common.api_helpers.mixins import PublicPrimaryKeyMixin, RateLimitHeadersMixin, UpdateSerializerMixin from common.api_helpers.paginators import FiftyPageSizePaginator +from common.insight_log import EntityEvent, write_resource_insight_log class ActionView(RateLimitHeadersMixin, PublicPrimaryKeyMixin, UpdateSerializerMixin, ModelViewSet): @@ -36,24 +36,28 @@ class ActionView(RateLimitHeadersMixin, PublicPrimaryKeyMixin, UpdateSerializerM def perform_create(self, serializer): serializer.save() - instance = serializer.instance - organization = self.request.auth.organization - user = self.request.user - description = f"Custom action {instance.name} was created" - create_organization_log(organization, user, OrganizationLogType.TYPE_CUSTOM_ACTION_CREATED, description) + write_resource_insight_log( + instance=serializer.instance, + author=self.request.user, + event=EntityEvent.CREATED, + ) def perform_update(self, serializer): - organization = self.request.auth.organization - user = self.request.user - old_state = serializer.instance.repr_settings_for_client_side_logging + prev_state = serializer.instance.insight_logs_serialized serializer.save() - new_state = serializer.instance.repr_settings_for_client_side_logging - description = f"Custom action {serializer.instance.name} was changed " f"from:\n{old_state}\nto:\n{new_state}" - create_organization_log(organization, user, OrganizationLogType.TYPE_CUSTOM_ACTION_CHANGED, description) + 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): - organization = self.request.auth.organization - user = self.request.user - description = f"Custom action {instance.name} was deleted" - create_organization_log(organization, user, OrganizationLogType.TYPE_CUSTOM_ACTION_DELETED, description) + write_resource_insight_log( + instance=instance, + author=self.request.user, + event=EntityEvent.DELETED, + ) instance.delete() diff --git a/engine/apps/public_api/views/escalation_chains.py b/engine/apps/public_api/views/escalation_chains.py index 515e8017..e24310cd 100644 --- a/engine/apps/public_api/views/escalation_chains.py +++ b/engine/apps/public_api/views/escalation_chains.py @@ -8,10 +8,10 @@ from apps.auth_token.auth import ApiTokenAuthentication from apps.public_api.serializers import EscalationChainSerializer from apps.public_api.serializers.escalation_chains import EscalationChainUpdateSerializer from apps.public_api.throttlers.user_throttle import UserThrottle -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.filters import ByTeamFilter from common.api_helpers.mixins import RateLimitHeadersMixin, UpdateSerializerMixin from common.api_helpers.paginators import FiftyPageSizePaginator +from common.insight_log import EntityEvent, write_resource_insight_log class EscalationChainView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelViewSet): @@ -48,38 +48,29 @@ class EscalationChainView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelVie def perform_create(self, serializer): serializer.save() - - instance = serializer.instance - description = f"Escalation chain {instance.name} was created" - create_organization_log( - instance.organization, - self.request.user, - OrganizationLogType.TYPE_ESCALATION_CHAIN_CREATED, - description, + write_resource_insight_log( + instance=serializer.instance, + author=self.request.user, + event=EntityEvent.CREATED, ) def perform_destroy(self, instance): - instance.delete() - - description = f"Escalation chain {instance.name} was deleted" - create_organization_log( - instance.organization, - self.request.user, - OrganizationLogType.TYPE_ESCALATION_CHAIN_DELETED, - description, + write_resource_insight_log( + instance=instance, + author=self.request.user, + event=EntityEvent.DELETED, ) + instance.delete() def perform_update(self, serializer): instance = serializer.instance - old_state = instance.repr_settings_for_client_side_logging - + prev_state = instance.insight_logs_serialized serializer.save() - - new_state = instance.repr_settings_for_client_side_logging - description = f"Escalation chain {instance.name} was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log( - instance.organization, - self.request.user, - OrganizationLogType.TYPE_ESCALATION_CHAIN_CHANGED, - description, + new_state = 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, ) diff --git a/engine/apps/public_api/views/escalation_policies.py b/engine/apps/public_api/views/escalation_policies.py index 15203f63..f6dbe4bc 100644 --- a/engine/apps/public_api/views/escalation_policies.py +++ b/engine/apps/public_api/views/escalation_policies.py @@ -7,9 +7,9 @@ from apps.alerts.models import EscalationPolicy from apps.auth_token.auth import ApiTokenAuthentication from apps.public_api.serializers import EscalationPolicySerializer, EscalationPolicyUpdateSerializer from apps.public_api.throttlers.user_throttle import UserThrottle -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.mixins import RateLimitHeadersMixin, UpdateSerializerMixin from common.api_helpers.paginators import FiftyPageSizePaginator +from common.insight_log import EntityEvent, write_resource_insight_log class EscalationPolicyView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelViewSet): @@ -50,36 +50,28 @@ class EscalationPolicyView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelVi def perform_create(self, serializer): serializer.save() - instance = serializer.instance - organization = self.request.auth.organization - user = self.request.user - escalation_chain = instance.escalation_chain - description = ( - f"Escalation step '{instance.step_type_verbal}' with order {instance.order} was created for " - f"escalation chain '{escalation_chain.name}'" + write_resource_insight_log( + instance=serializer.instance, + author=self.request.user, + event=EntityEvent.CREATED, ) - create_organization_log(organization, user, OrganizationLogType.TYPE_ESCALATION_STEP_CREATED, description) def perform_update(self, serializer): - organization = self.request.auth.organization - user = self.request.user - old_state = serializer.instance.repr_settings_for_client_side_logging + prev_state = serializer.instance.insight_logs_serialized serializer.save() - new_state = serializer.instance.repr_settings_for_client_side_logging - escalation_chain = serializer.instance.escalation_chain - description = ( - f"Settings for escalation step of escalation chain '{escalation_chain.name}' was changed " - f"from:\n{old_state}\nto:\n{new_state}" + 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, ) - create_organization_log(organization, user, OrganizationLogType.TYPE_ESCALATION_STEP_CHANGED, description) def perform_destroy(self, instance): - organization = self.request.auth.organization - user = self.request.user - escalation_chain = instance.escalation_chain - description = ( - f"Escalation step '{instance.step_type_verbal}' with order {instance.order} of " - f"escalation chain '{escalation_chain.name}' was deleted" + write_resource_insight_log( + instance=instance, + author=self.request.user, + event=EntityEvent.DELETED, ) - create_organization_log(organization, user, OrganizationLogType.TYPE_ESCALATION_STEP_DELETED, description) instance.delete() diff --git a/engine/apps/public_api/views/integrations.py b/engine/apps/public_api/views/integrations.py index 447c5b2b..36ef6ea3 100644 --- a/engine/apps/public_api/views/integrations.py +++ b/engine/apps/public_api/views/integrations.py @@ -8,10 +8,10 @@ from apps.alerts.models import AlertReceiveChannel from apps.auth_token.auth import ApiTokenAuthentication from apps.public_api.serializers import IntegrationSerializer, IntegrationUpdateSerializer from apps.public_api.throttlers.user_throttle import UserThrottle -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.filters import ByTeamFilter from common.api_helpers.mixins import FilterSerializerMixin, RateLimitHeadersMixin, UpdateSerializerMixin from common.api_helpers.paginators import FiftyPageSizePaginator +from common.insight_log import EntityEvent, write_resource_insight_log from .maintaiable_object_mixin import MaintainableObjectMixin @@ -58,20 +58,17 @@ class IntegrationView( raise NotFound def perform_update(self, serializer): - old_state = serializer.instance.repr_settings_for_client_side_logging + prev_state = serializer.instance.insight_logs_serialized serializer.save() - new_state = serializer.instance.repr_settings_for_client_side_logging - description = f"Integration settings was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log( - serializer.instance.organization, - self.request.user, - OrganizationLogType.TYPE_INTEGRATION_CHANGED, - description, + 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): - organization = instance.organization - user = self.request.user - description = f"Integration {instance.verbal_name} was deleted" - create_organization_log(organization, user, OrganizationLogType.TYPE_INTEGRATION_DELETED, description) + write_resource_insight_log(instance=instance, author=self.request.user, event=EntityEvent.DELETED) instance.delete() diff --git a/engine/apps/public_api/views/on_call_shifts.py b/engine/apps/public_api/views/on_call_shifts.py index 1d0df97a..7b904b6a 100644 --- a/engine/apps/public_api/views/on_call_shifts.py +++ b/engine/apps/public_api/views/on_call_shifts.py @@ -7,10 +7,10 @@ from apps.auth_token.auth import ApiTokenAuthentication from apps.public_api.serializers import CustomOnCallShiftSerializer, CustomOnCallShiftUpdateSerializer from apps.public_api.throttlers.user_throttle import UserThrottle from apps.schedules.models import CustomOnCallShift -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.filters import ByTeamFilter from common.api_helpers.mixins import RateLimitHeadersMixin, UpdateSerializerMixin from common.api_helpers.paginators import FiftyPageSizePaginator +from common.insight_log import EntityEvent, write_resource_insight_log class CustomOnCallShiftView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelViewSet): @@ -52,28 +52,28 @@ class CustomOnCallShiftView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelV def perform_create(self, serializer): serializer.save() - instance = serializer.instance - organization = self.request.auth.organization - user = self.request.user - description = ( - f"Custom on-call shift with params: {instance.repr_settings_for_client_side_logging} " f"was created" + write_resource_insight_log( + instance=serializer.instance, + author=self.request.user, + event=EntityEvent.CREATED, ) - create_organization_log(organization, user, OrganizationLogType.TYPE_ON_CALL_SHIFT_CREATED, description) def perform_update(self, serializer): - organization = self.request.auth.organization - user = self.request.user - old_state = serializer.instance.repr_settings_for_client_side_logging + prev_state = serializer.instance.insight_logs_serialized serializer.save() - new_state = serializer.instance.repr_settings_for_client_side_logging - description = f"Settings of custom on-call shift was changed " f"from:\n{old_state}\nto:\n{new_state}" - create_organization_log(organization, user, OrganizationLogType.TYPE_ON_CALL_SHIFT_CHANGED, description) + 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): - organization = self.request.auth.organization - user = self.request.user - description = ( - f"Custom on-call shift " f"with params: {instance.repr_settings_for_client_side_logging} was deleted" + write_resource_insight_log( + instance=instance, + author=self.request.user, + event=EntityEvent.DELETED, ) - create_organization_log(organization, user, OrganizationLogType.TYPE_ON_CALL_SHIFT_DELETED, description) instance.delete() diff --git a/engine/apps/public_api/views/personal_notifications.py b/engine/apps/public_api/views/personal_notifications.py index 3119bea9..44b251a3 100644 --- a/engine/apps/public_api/views/personal_notifications.py +++ b/engine/apps/public_api/views/personal_notifications.py @@ -9,10 +9,10 @@ from apps.base.models import UserNotificationPolicy from apps.public_api.serializers import PersonalNotificationRuleSerializer, PersonalNotificationRuleUpdateSerializer from apps.public_api.throttlers.user_throttle import UserThrottle from apps.user_management.models import User -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.exceptions import BadRequest from common.api_helpers.mixins import RateLimitHeadersMixin, UpdateSerializerMixin from common.api_helpers.paginators import FiftyPageSizePaginator +from common.insight_log import EntityEvent, write_resource_insight_log class PersonalNotificationView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelViewSet): @@ -72,45 +72,40 @@ class PersonalNotificationView(RateLimitHeadersMixin, UpdateSerializerMixin, Mod return Response(status=status.HTTP_204_NO_CONTENT) def perform_destroy(self, instance): - organization = self.request.auth.organization user = self.request.user - old_state = user.repr_settings_for_client_side_logging + prev_state = user.insight_logs_serialized instance.delete() - new_state = user.repr_settings_for_client_side_logging - description = f"User settings for user {user.username} was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log( - organization, - user, - OrganizationLogType.TYPE_USER_SETTINGS_CHANGED, - description, + new_state = user.insight_logs_serialized + write_resource_insight_log( + instance=user, + author=self.request.user, + event=EntityEvent.UPDATED, + prev_state=prev_state, + new_state=new_state, ) def perform_create(self, serializer): - organization = self.request.auth.organization - author = self.request.user user = serializer.validated_data["user"] - - old_state = user.repr_settings_for_client_side_logging + prev_state = user.insight_logs_serialized serializer.save() - new_state = user.repr_settings_for_client_side_logging - description = f"User settings for user {user.username} was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log( - organization, - author, - OrganizationLogType.TYPE_USER_SETTINGS_CHANGED, - description, + new_state = user.insight_logs_serialized + write_resource_insight_log( + instance=user, + author=self.request.user, + event=EntityEvent.UPDATED, + prev_state=prev_state, + new_state=new_state, ) def perform_update(self, serializer): - organization = self.request.auth.organization user = self.request.user - old_state = user.repr_settings_for_client_side_logging + prev_state = user.insight_logs_serialized serializer.save() - new_state = user.repr_settings_for_client_side_logging - description = f"User settings for user {user.username} was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log( - organization, - user, - OrganizationLogType.TYPE_USER_SETTINGS_CHANGED, - description, + new_state = user.insight_logs_serialized + write_resource_insight_log( + instance=user, + author=self.request.user, + event=EntityEvent.UPDATED, + prev_state=prev_state, + new_state=new_state, ) diff --git a/engine/apps/public_api/views/routes.py b/engine/apps/public_api/views/routes.py index c7afa492..52d62eda 100644 --- a/engine/apps/public_api/views/routes.py +++ b/engine/apps/public_api/views/routes.py @@ -9,10 +9,10 @@ from apps.alerts.models import ChannelFilter from apps.auth_token.auth import ApiTokenAuthentication from apps.public_api.serializers import ChannelFilterSerializer, ChannelFilterUpdateSerializer from apps.public_api.throttlers.user_throttle import UserThrottle -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.exceptions import BadRequest from common.api_helpers.mixins import RateLimitHeadersMixin, UpdateSerializerMixin from common.api_helpers.paginators import TwentyFivePageSizePaginator +from common.insight_log import EntityEvent, write_resource_insight_log class ChannelFilterView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelViewSet): @@ -60,43 +60,30 @@ class ChannelFilterView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelViewS if instance.is_default: raise BadRequest(detail="Unable to delete default filter") else: - alert_receive_channel = instance.alert_receive_channel - user = self.request.user - route_verbal = instance.verbal_name_for_clients.capitalize() - description = f"{route_verbal} of integration {alert_receive_channel.verbal_name} was deleted" - create_organization_log( - alert_receive_channel.organization, - user, - OrganizationLogType.TYPE_CHANNEL_FILTER_DELETED, - description, + write_resource_insight_log( + instance=instance, + author=self.request.user, + event=EntityEvent.DELETED, ) self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) def perform_create(self, serializer): serializer.save() - instance = serializer.instance - alert_receive_channel = instance.alert_receive_channel - user = self.request.user - route_verbal = instance.verbal_name_for_clients.capitalize() - description = f"{route_verbal} was created for integration {alert_receive_channel.verbal_name}" - create_organization_log( - alert_receive_channel.organization, - user, - OrganizationLogType.TYPE_CHANNEL_FILTER_CREATED, - description, + write_resource_insight_log( + instance=serializer.instance, + author=self.request.user, + event=EntityEvent.CREATED, ) def perform_update(self, serializer): - organization = self.request.auth.organization - user = self.request.user - old_state = serializer.instance.repr_settings_for_client_side_logging + prev_state = serializer.instance.insight_logs_serialized serializer.save() - new_state = serializer.instance.repr_settings_for_client_side_logging - alert_receive_channel = serializer.instance.alert_receive_channel - route_verbal = serializer.instance.verbal_name_for_clients.capitalize() - description = ( - f"Settings for {route_verbal} of integration {alert_receive_channel.verbal_name} " - f"was changed from:\n{old_state}\nto:\n{new_state}" + 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, ) - create_organization_log(organization, user, OrganizationLogType.TYPE_CHANNEL_FILTER_CHANGED, description) diff --git a/engine/apps/public_api/views/schedules.py b/engine/apps/public_api/views/schedules.py index e52d72fc..8029de12 100644 --- a/engine/apps/public_api/views/schedules.py +++ b/engine/apps/public_api/views/schedules.py @@ -13,11 +13,11 @@ from apps.public_api.throttlers.user_throttle import UserThrottle from apps.schedules.ical_utils import ical_export_from_schedule from apps.schedules.models import OnCallSchedule, OnCallScheduleWeb from apps.slack.tasks import update_slack_user_group_for_schedules -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.api_helpers.exceptions import BadRequest from common.api_helpers.filters import ByTeamFilter from common.api_helpers.mixins import RateLimitHeadersMixin, UpdateSerializerMixin from common.api_helpers.paginators import FiftyPageSizePaginator +from common.insight_log import EntityEvent, write_resource_insight_log class OnCallScheduleChannelView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelViewSet): @@ -65,18 +65,17 @@ class OnCallScheduleChannelView(RateLimitHeadersMixin, UpdateSerializerMixin, Mo if instance.user_group is not None: update_slack_user_group_for_schedules.apply_async((instance.user_group.pk,)) - organization = self.request.auth.organization - user = self.request.user - description = f"Schedule {instance.name} was created" - create_organization_log(organization, user, OrganizationLogType.TYPE_SCHEDULE_CREATED, description) + write_resource_insight_log( + instance=serializer.instance, + author=self.request.user, + event=EntityEvent.CREATED, + ) def perform_update(self, serializer): if isinstance(serializer.instance, OnCallScheduleWeb): raise BadRequest(detail="Web schedule update is not enabled through API") - organization = self.request.auth.organization - user = self.request.user - old_state = serializer.instance.repr_settings_for_client_side_logging + prev_state = serializer.instance.insight_logs_serialized old_user_group = serializer.instance.user_group updated_schedule = serializer.save() @@ -87,15 +86,21 @@ class OnCallScheduleChannelView(RateLimitHeadersMixin, UpdateSerializerMixin, Mo if updated_schedule.user_group is not None and updated_schedule.user_group != old_user_group: update_slack_user_group_for_schedules.apply_async((updated_schedule.user_group.pk,)) - new_state = serializer.instance.repr_settings_for_client_side_logging - description = f"Schedule {serializer.instance.name} was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log(organization, user, OrganizationLogType.TYPE_SCHEDULE_CHANGED, description) + 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): - organization = self.request.auth.organization - user = self.request.user - description = f"Schedule {instance.name} was deleted" - create_organization_log(organization, user, OrganizationLogType.TYPE_SCHEDULE_DELETED, description) + write_resource_insight_log( + instance=instance, + author=self.request.user, + event=EntityEvent.DELETED, + ) instance.delete() diff --git a/engine/apps/schedules/models/custom_on_call_shift.py b/engine/apps/schedules/models/custom_on_call_shift.py index 88c82a44..030f2cb0 100644 --- a/engine/apps/schedules/models/custom_on_call_shift.py +++ b/engine/apps/schedules/models/custom_on_call_shift.py @@ -513,3 +513,65 @@ class CustomOnCallShift(models.Model): name = f"{schedule.name}-{shift_type_name}-{priority_level}-" name += "".join(random.choice(string.ascii_lowercase) for _ in range(5)) return name + + # Insight logs + @property + def insight_logs_type_verbal(self): + return "oncall_shift" + + @property + def insight_logs_verbal(self): + return self.name + + @property + def insight_logs_serialized(self): + users_verbal = [] + if self.type == CustomOnCallShift.TYPE_ROLLING_USERS_EVENT: + if self.rolling_users is not None: + for users_dict in self.rolling_users: + users = self.organization.users.filter(public_primary_key__in=users_dict.values()) + users_verbal.extend([user.username for user in users]) + else: + users = self.users.all() + users_verbal = [user.username for user in users] + result = { + "name": self.name, + "source": self.get_source_display(), + "type": self.get_type_display(), + "users": users_verbal, + "start": self.start.isoformat(), + "duration": self.duration.seconds, + "priority_level": self.priority_level, + } + if self.type not in (CustomOnCallShift.TYPE_SINGLE_EVENT, CustomOnCallShift.TYPE_OVERRIDE): + result["frequency"] = self.get_frequency_display() + result["interval"] = self.interval + result["week_start"] = self.week_start + result["by_day"] = self.by_day + result["by_month"] = self.by_month + result["by_monthday"] = self.by_monthday + result["rotation_start"] = self.rotation_start.isoformat() + if self.until: + result["until"] = self.until.isoformat() + if self.team: + result["team"] = self.team.name + result["team_id"] = self.team.public_primary_key + else: + result["team"] = "General" + if self.time_zone: + result["time_zone"] = self.time_zone + return result + + @property + def insight_logs_metadata(self): + result = {} + if self.team: + result["team"] = self.team.name + result["team_id"] = self.team.public_primary_key + else: + result["team"] = "General" + if self.schedule: + result["schedule"] = self.schedule.insight_logs_verbal + result["schedule_id"] = self.schedule.public_primary_key + + return result diff --git a/engine/apps/schedules/models/on_call_schedule.py b/engine/apps/schedules/models/on_call_schedule.py index 98d605f3..98350f8e 100644 --- a/engine/apps/schedules/models/on_call_schedule.py +++ b/engine/apps/schedules/models/on_call_schedule.py @@ -133,36 +133,6 @@ class OnCallSchedule(PolymorphicModel): class Meta: unique_together = ("name", "organization") - @property - def repr_settings_for_client_side_logging(self): - """ - Example of execution: - name: test, team: example, url: None - slack reminder settings: notification frequency: Each shift, current shift notification: Yes, - next shift notification: No, action for slot when no one is on-call: Notify all people in the channel - """ - result = f"name: {self.name}, team: {self.team.name if self.team else 'No team'}" - - if self.organization.slack_team_identity: - if self.channel: - SlackChannel = apps.get_model("slack", "SlackChannel") - sti = self.organization.slack_team_identity - slack_channel = SlackChannel.objects.filter(slack_team_identity=sti, slack_id=self.channel).first() - if slack_channel: - result += f", slack channel: {slack_channel.name}" - - if self.user_group is not None: - result += f", user group: {self.user_group.handle}" - - result += ( - f"\nslack reminder settings: " - f"notification frequency: {self.get_notify_oncall_shift_freq_display()}, " - f"current shift notification: {'Yes' if self.mention_oncall_start else 'No'}, " - f"next shift notification: {'Yes' if self.mention_oncall_next else 'No'}, " - f"action for slot when no one is on-call: {self.get_notify_empty_oncall_display()}" - ) - return result - def get_icalendars(self): """Returns list of calendars. Primary calendar should always be the first""" calendar_primary = None @@ -368,6 +338,47 @@ class OnCallSchedule(PolymorphicModel): resolved.sort(key=lambda e: (e["start"], e["shift"]["pk"])) return resolved + # Insight logs + @property + def insight_logs_verbal(self): + return self.name + + @property + def insight_logs_serialized(self): + result = { + "name": self.name, + } + if self.team: + result["team"] = self.team.name + result["team_id"] = self.team.public_primary_key + else: + result["team"] = "General" + if self.organization.slack_team_identity: + if self.channel: + SlackChannel = apps.get_model("slack", "SlackChannel") + sti = self.organization.slack_team_identity + slack_channel = SlackChannel.objects.filter(slack_team_identity=sti, slack_id=self.channel).first() + if slack_channel: + result["slack_channel"] = slack_channel.name + if self.user_group is not None: + result["user_group"] = self.user_group.handle + + result["notification_frequency"] = self.get_notify_oncall_shift_freq_display() + result["current_shift_notification"] = self.mention_oncall_start + result["next_shift_notification"] = self.mention_oncall_next + result["notify_empty_oncall"] = self.get_notify_empty_oncall_display + return result + + @property + def insight_logs_metadata(self): + result = {} + if self.team: + result["team"] = self.team.name + result["team_id"] = self.team.public_primary_key + else: + result["team"] = "General" + return result + class OnCallScheduleICal(OnCallSchedule): # For the ical schedule both primary and overrides icals are imported via ical url @@ -421,13 +432,17 @@ class OnCallScheduleICal(OnCallSchedule): ) self.save(update_fields=["cached_ical_file_overrides", "prev_ical_file_overrides", "ical_file_error_overrides"]) + # Insight logs @property - def repr_settings_for_client_side_logging(self): - result = super().repr_settings_for_client_side_logging - result += ( - f", primary calendar url: {self.ical_url_primary}, " f"overrides calendar url: {self.ical_url_overrides}" - ) - return result + def insight_logs_serialized(self): + res = super().insight_logs_serialized + res["primary_calendar_url"] = self.ical_url_primary + res["overrides_calendar_url"] = self.ical_url_overrides + return res + + @property + def insight_logs_type_verbal(self): + return "ical_schedule" class OnCallScheduleCalendar(OnCallSchedule): @@ -501,10 +516,14 @@ class OnCallScheduleCalendar(OnCallSchedule): return ical @property - def repr_settings_for_client_side_logging(self): - result = super().repr_settings_for_client_side_logging - result += f", overrides calendar url: {self.ical_url_overrides}" - return result + def insight_logs_type_verbal(self): + return "calendar_schedule" + + @property + def insight_logs_serialized(self): + res = super().insight_logs_serialized + res["overrides_calendar_url"] = self.ical_url_overrides + return res class OnCallScheduleWeb(OnCallSchedule): @@ -598,3 +617,14 @@ class OnCallScheduleWeb(OnCallSchedule): setattr(self, ical_attr, original_value) return shift_events, final_events + + # Insight logs + @property + def insight_logs_type_verbal(self): + return "web_schedule" + + @property + def insight_logs_serialized(self): + res = super().insight_logs_serialized + res["time_zone"] = self.time_zone + return res diff --git a/engine/apps/slack/models/slack_team_identity.py b/engine/apps/slack/models/slack_team_identity.py index b8d76ce9..3abefc6e 100644 --- a/engine/apps/slack/models/slack_team_identity.py +++ b/engine/apps/slack/models/slack_team_identity.py @@ -7,8 +7,8 @@ from django.db.models import JSONField from apps.slack.constants import SLACK_INVALID_AUTH_RESPONSE, SLACK_WRONG_TEAM_NAMES from apps.slack.slack_client import SlackClientWithErrorHandling from apps.slack.slack_client.exceptions import SlackAPIException, SlackAPITokenException -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.constants.role import Role +from common.insight_log.chatops_insight_logs import ChatOpsEvent, ChatOpsType, write_chatops_insight_log logger = logging.getLogger(__name__) @@ -63,8 +63,9 @@ class SlackTeamIdentity(models.Model): self.cached_reinstall_data = None self.installed_via_granular_permissions = True self.save() - description = f"Slack workspace {self.cached_name} was connected to organization" - create_organization_log(organization, user, OrganizationLogType.TYPE_SLACK_WORKSPACE_CONNECTED, description) + write_chatops_insight_log( + author=user, event_name=ChatOpsEvent.WORKSPACE_CONNECTED, chatops_type=ChatOpsType.SLACK + ) def get_cached_channels(self, search_term=None, slack_id=None): queryset = self.cached_channels diff --git a/engine/apps/slack/scenarios/alertgroup_appearance.py b/engine/apps/slack/scenarios/alertgroup_appearance.py index 588b70d0..7b772a0e 100644 --- a/engine/apps/slack/scenarios/alertgroup_appearance.py +++ b/engine/apps/slack/scenarios/alertgroup_appearance.py @@ -6,8 +6,8 @@ from jinja2 import TemplateSyntaxError from rest_framework.response import Response from apps.slack.scenarios import scenario_step -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from common.constants.role import Role +from common.insight_log import EntityEvent, write_resource_insight_log from common.jinja_templater import jinja_template_env from .step_mixins import CheckAlertIsUnarchivedMixin, IncidentActionsAccessControlMixin @@ -233,7 +233,7 @@ class UpdateAppearanceStep(scenario_step.ScenarioStep): alert_group = AlertGroup.all_objects.filter(pk=alert_group_pk).select_for_update().get() integration = alert_group.channel.integration alert_receive_channel = alert_group.channel - old_state = alert_receive_channel.repr_settings_for_client_side_logging + prev_state = alert_receive_channel.insight_logs_serialized for templatizable_attr in ["title", "message", "image_url"]: for notification_channel in ["slack", "web", "sms", "phone_call", "email", "telegram"]: @@ -308,12 +308,15 @@ class UpdateAppearanceStep(scenario_step.ScenarioStep): headers={"content-type": "application/json"}, ) - new_state = alert_receive_channel.repr_settings_for_client_side_logging + new_state = alert_receive_channel.insight_logs_serialized - if new_state != old_state: - description = f"Integration settings was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log( - self.organization, self.user, OrganizationLogType.TYPE_INTEGRATION_CHANGED, description + if new_state != prev_state: + write_resource_insight_log( + instance=alert_receive_channel, + author=slack_user_identity.get_user(alert_receive_channel.organization), + event=EntityEvent.UPDATED, + prev_state=prev_state, + new_state=new_state, ) attachments = alert_group.render_slack_attachments() diff --git a/engine/apps/slack/scenarios/schedules.py b/engine/apps/slack/scenarios/schedules.py index 9c5155a4..c7741e7c 100644 --- a/engine/apps/slack/scenarios/schedules.py +++ b/engine/apps/slack/scenarios/schedules.py @@ -6,7 +6,7 @@ from django.utils import timezone from apps.schedules.models import OnCallSchedule from apps.slack.scenarios import scenario_step from apps.slack.utils import format_datetime_to_slack -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log +from common.insight_log import EntityEvent, write_resource_insight_log class EditScheduleShiftNotifyStep(scenario_step.ScenarioStep): @@ -57,16 +57,16 @@ class EditScheduleShiftNotifyStep(scenario_step.ScenarioStep): private_metadata = json.loads(payload["view"]["private_metadata"]) schedule_id = private_metadata["schedule_id"] schedule = OnCallSchedule.objects.get(pk=schedule_id) - old_state = schedule.repr_settings_for_client_side_logging + prev_state = schedule.insight_logs_serialized setattr(schedule, action["block_id"], int(action["selected_option"]["value"])) schedule.save() - new_state = schedule.repr_settings_for_client_side_logging - description = f"Schedule {schedule.name} was changed from:\n{old_state}\nto:\n{new_state}" - create_organization_log( - schedule.organization, - slack_user_identity.get_user(schedule.organization), - OrganizationLogType.TYPE_SCHEDULE_CHANGED, - description, + new_state = schedule.insight_logs_serialized + write_resource_insight_log( + instance=schedule, + author=slack_user_identity.get_user(schedule.organization), + event=EntityEvent.UPDATED, + prev_state=prev_state, + new_state=new_state, ) def get_modal_blocks(self, schedule_id): diff --git a/engine/apps/slack/views.py b/engine/apps/slack/views.py index 7cb2e077..217b0f7c 100644 --- a/engine/apps/slack/views.py +++ b/engine/apps/slack/views.py @@ -51,7 +51,7 @@ from apps.slack.scenarios.slack_usergroup import STEPS_ROUTING as SLACK_USERGROU from apps.slack.slack_client import SlackClientWithErrorHandling from apps.slack.slack_client.exceptions import SlackAPIException, SlackAPITokenException from apps.slack.tasks import clean_slack_integration_leftovers, unpopulate_slack_user_identities -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log +from common.insight_log import ChatOpsEvent, ChatOpsType, write_chatops_insight_log from .models import SlackActionRecord, SlackMessage, SlackTeamIdentity, SlackUserIdentity @@ -537,9 +537,10 @@ class ResetSlackView(APIView): slack_team_identity = organization.slack_team_identity if slack_team_identity is not None: clean_slack_integration_leftovers.apply_async((organization.pk,)) - description = f"Slack workspace {slack_team_identity.cached_name} was disconnected from organization" - create_organization_log( - organization, request.user, OrganizationLogType.TYPE_SLACK_WORKSPACE_DISCONNECTED, description + write_chatops_insight_log( + author=request.user, + event_name=ChatOpsEvent.WORKSPACE_DISCONNECTED, + chatops_type=ChatOpsType.SLACK, ) unpopulate_slack_user_identities(organization.pk, True) response = Response(status=200) diff --git a/engine/apps/social_auth/pipeline.py b/engine/apps/social_auth/pipeline.py index 436799a1..1babe21d 100644 --- a/engine/apps/social_auth/pipeline.py +++ b/engine/apps/social_auth/pipeline.py @@ -12,6 +12,7 @@ from common.constants.slack_auth import ( SLACK_AUTH_SLACK_USER_ALREADY_CONNECTED_ERROR, SLACK_AUTH_WRONG_WORKSPACE_ERROR, ) +from common.insight_log import ChatOpsEvent, ChatOpsType, write_chatops_insight_log logger = logging.getLogger(__name__) @@ -66,6 +67,14 @@ def connect_user_to_slack(response, backend, strategy, user, organization, *args "cached_slack_email": response["user"]["email"], }, ) + + write_chatops_insight_log( + author=user, + event_name=ChatOpsEvent.USER_LINKED, + chatops_type=ChatOpsType.SLACK, + user=user.username, + user_id=user.public_primary_key, + ) user.slack_user_identity = slack_user_identity user.save(update_fields=["slack_user_identity"]) diff --git a/engine/apps/telegram/models/connectors/channel.py b/engine/apps/telegram/models/connectors/channel.py index c21e76c9..75190f6b 100644 --- a/engine/apps/telegram/models/connectors/channel.py +++ b/engine/apps/telegram/models/connectors/channel.py @@ -10,7 +10,7 @@ from telegram import error from apps.alerts.models import AlertGroup from apps.telegram.client import TelegramClient from apps.telegram.models import TelegramMessage -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log +from common.insight_log.chatops_insight_logs import ChatOpsEvent, ChatOpsType, write_chatops_insight_log from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length logger = logging.getLogger(__name__) @@ -99,17 +99,12 @@ class TelegramToOrganizationConnector(models.Model): self.is_default_channel = True self.save(update_fields=["is_default_channel"]) - - description = ( - f"The default channel for incidents in Telegram was changed " - f"{f'from @{old_default_channel.channel_name} ' if old_default_channel else ''}" - f"to @{self.channel_name}" - ) - create_organization_log( - self.organization, - author, - OrganizationLogType.TYPE_TELEGRAM_DEFAULT_CHANNEL_CHANGED, - description, + write_chatops_insight_log( + author=author, + event_name=ChatOpsEvent.DEFAULT_CHANNEL_CHANGED, + chatops_type=ChatOpsType.TELEGRAM, + prev_channel=old_default_channel.channel_name if old_default_channel else None, + new_channel=self.channel_name, ) def send_alert_group_message(self, alert_group: AlertGroup) -> None: diff --git a/engine/apps/telegram/models/verification/channel.py b/engine/apps/telegram/models/verification/channel.py index e3a7ce1a..ef8e8b77 100644 --- a/engine/apps/telegram/models/verification/channel.py +++ b/engine/apps/telegram/models/verification/channel.py @@ -6,7 +6,7 @@ from django.db import models from django.utils import timezone from apps.telegram.models import TelegramToOrganizationConnector -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log +from common.insight_log.chatops_insight_logs import ChatOpsEvent, ChatOpsType, write_chatops_insight_log class TelegramChannelVerificationCode(models.Model): @@ -50,21 +50,19 @@ class TelegramChannelVerificationCode(models.Model): }, ) - description = f"Telegram channel @{channel_name} was connected to organization" - create_organization_log( - verification_code.organization, - verification_code.author, - OrganizationLogType.TYPE_TELEGRAM_CHANNEL_CONNECTED, - description, + write_chatops_insight_log( + author=verification_code.author, + event_name=ChatOpsEvent.CHANNEL_CONNECTED, + chatops_type=ChatOpsType.TELEGRAM, + channel_name=channel_name, ) - if not connector_exists: - description = f"The default channel for incidents in Telegram was changed to @{channel_name}" - create_organization_log( - verification_code.organization, - verification_code.author, - OrganizationLogType.TYPE_TELEGRAM_DEFAULT_CHANNEL_CHANGED, - description, + write_chatops_insight_log( + author=verification_code.author, + event_name=ChatOpsEvent.DEFAULT_CHANNEL_CHANGED, + chatops_type=ChatOpsType.TELEGRAM, + prev_channel=None, + new_channel=channel_name, ) return connector, created diff --git a/engine/apps/telegram/models/verification/personal.py b/engine/apps/telegram/models/verification/personal.py index c2bdf051..9dc49cbc 100644 --- a/engine/apps/telegram/models/verification/personal.py +++ b/engine/apps/telegram/models/verification/personal.py @@ -6,7 +6,7 @@ from django.db import IntegrityError, models from django.utils import timezone from apps.telegram.models import TelegramToUserConnector -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log +from common.insight_log import ChatOpsEvent, ChatOpsType, write_chatops_insight_log class TelegramVerificationCode(models.Model): @@ -32,13 +32,12 @@ class TelegramVerificationCode(models.Model): connector, created = TelegramToUserConnector.objects.get_or_create( user=user, defaults={"telegram_nick_name": telegram_nick_name, "telegram_chat_id": telegram_chat_id} ) - - description = f"Telegram account of user {user.username} was connected" - create_organization_log( - user.organization, - user, - OrganizationLogType.TYPE_TELEGRAM_TO_USER_CONNECTED, - description, + write_chatops_insight_log( + author=user, + event_name=ChatOpsEvent.USER_LINKED, + chatops_type=ChatOpsType.TELEGRAM, + user=user.username, + user_id=user.public_primary_key, ) return connector, created diff --git a/engine/apps/user_management/models/organization.py b/engine/apps/user_management/models/organization.py index aa22f589..fd37ba81 100644 --- a/engine/apps/user_management/models/organization.py +++ b/engine/apps/user_management/models/organization.py @@ -10,8 +10,8 @@ from mirage import fields as mirage_fields from apps.alerts.models import MaintainableObject from apps.alerts.tasks import disable_maintenance from apps.slack.utils import post_message_to_channel -from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log from apps.user_management.subscription_strategy import FreePublicBetaSubscriptionStrategy +from common.insight_log import ChatOpsEvent, ChatOpsType, write_chatops_insight_log from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length logger = logging.getLogger(__name__) @@ -232,31 +232,13 @@ class Organization(MaintainableObject): old_channel_name = old_general_log_channel_id.name if old_general_log_channel_id else None self.general_log_channel_id = channel_id self.save(update_fields=["general_log_channel_id"]) - description = ( - f"The default channel for incidents in Slack changed " - f"{f'from #{old_channel_name} ' if old_channel_name else ''}to #{channel_name}" + write_chatops_insight_log( + author=user, + event_name=ChatOpsEvent.DEFAULT_CHANNEL_CHANGED, + chatops_type=ChatOpsType.SLACK, + prev_channel=old_channel_name, + new_channel=channel_name, ) - create_organization_log(self, user, OrganizationLogType.TYPE_SLACK_DEFAULT_CHANNEL_CHANGED, description) - - @property - def repr_settings_for_client_side_logging(self): - """ - Example of execution: - # TODO: 770: check format - name: Test, archive alerts from date: 2019-10-24, require resolution note: No - acknowledge remind settings: Never remind about ack-ed incidents, and never unack - """ - result = ( - f"name: {self.org_title}, " - f"archive alerts from date: {self.archive_alerts_from.isoformat()}, " - f"require resolution note: {'Yes' if self.is_resolution_note_required else 'No'}" - ) - if self.slack_team_identity: - result += ( - f"\nacknowledge remind settings: {self.get_acknowledge_remind_timeout_display()}, " - f"{self.get_unacknowledge_timeout_display()}, " - ) - return result @property def web_link(self): @@ -264,3 +246,24 @@ class Organization(MaintainableObject): def __str__(self): return f"{self.pk}: {self.org_title}" + + # Insight logs + @property + def insight_logs_type_verbal(self): + return "organization" + + @property + def insight_logs_verbal(self): + return self.org_title + + @property + def insight_logs_serialized(self): + return { + "name": self.org_title, + "is_resolution_note_required": self.is_resolution_note_required, + "archive_alerts_from": self.archive_alerts_from.isoformat(), + } + + @property + def insight_logs_metadata(self): + return {} diff --git a/engine/apps/user_management/models/user.py b/engine/apps/user_management/models/user.py index 6e8b27dc..c1a00669 100644 --- a/engine/apps/user_management/models/user.py +++ b/engine/apps/user_management/models/user.py @@ -208,31 +208,6 @@ class User(models.Model): return verbal - @property - def repr_settings_for_client_side_logging(self): - """ - Example of execution: - username: Alex, role: Admin, verified phone number: not added, unverified phone number: not added, - telegram connected: No, - notification policies: default: SMS - 5 min - :telephone:, important: :telephone: - """ - UserNotificationPolicy = apps.get_model("base", "UserNotificationPolicy") - - default, important = UserNotificationPolicy.get_short_verbals_for_user(user=self) - notification_policies_verbal = f"default: {' - '.join(default)}, important: {' - '.join(important)}" - notification_policies_verbal = demojize(notification_policies_verbal) - - result = ( - f"username: {self.username}, role: {self.get_role_display()}, " - f"verified phone number: " - f"{self.verified_phone_number if self.verified_phone_number else 'not added'}, " - f"unverified phone number: " - f"{self.unverified_phone_number if self.unverified_phone_number else 'not added'}, " - f"telegram connected: {'Yes' if self.is_telegram_connected else 'No'}" - f"\nnotification policies: {notification_policies_verbal}" - ) - return result - @property def timezone(self): if self._timezone: @@ -250,6 +225,37 @@ class User(models.Model): def short(self): return {"username": self.username, "pk": self.public_primary_key, "avatar": self.avatar_url} + # Insight logs + @property + def insight_logs_type_verbal(self): + return "user" + + @property + def insight_logs_verbal(self): + return self.username + + @property + def insight_logs_serialized(self): + UserNotificationPolicy = apps.get_model("base", "UserNotificationPolicy") + default, important = UserNotificationPolicy.get_short_verbals_for_user(user=self) + notification_policies_verbal = f"default: {' - '.join(default)}, important: {' - '.join(important)}" + notification_policies_verbal = demojize(notification_policies_verbal) + + result = { + "username": self.username, + "role": self.get_role_display(), + "notification_policies": notification_policies_verbal, + } + if self.verified_phone_number: + result["verified_phone_number"] = self.unverified_phone_number + if self.unverified_phone_number: + result["unverified_phone_number"] = self.unverified_phone_number + return result + + @property + def insight_logs_metadata(self): + return {} + # TODO: check whether this signal can be moved to save method of the model @receiver(post_save, sender=User) diff --git a/engine/apps/user_management/organization_log_creator/__init__.py b/engine/apps/user_management/organization_log_creator/__init__.py deleted file mode 100644 index 5ce5c890..00000000 --- a/engine/apps/user_management/organization_log_creator/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .create_organization_log import create_organization_log # noqa: F401 -from .organization_log_type import OrganizationLogType # noqa: F401 diff --git a/engine/apps/user_management/organization_log_creator/create_organization_log.py b/engine/apps/user_management/organization_log_creator/create_organization_log.py deleted file mode 100644 index 5430e5e7..00000000 --- a/engine/apps/user_management/organization_log_creator/create_organization_log.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.apps import apps - - -def create_organization_log(organization, author, type, description): - OrganizationLogRecord = apps.get_model("base", "OrganizationLogRecord") - OrganizationLogRecord.objects.create( - organization=organization, - author=author, - type=type, - description=description, - ) diff --git a/engine/apps/user_management/organization_log_creator/organization_log_type.py b/engine/apps/user_management/organization_log_creator/organization_log_type.py deleted file mode 100644 index 5f1f05fb..00000000 --- a/engine/apps/user_management/organization_log_creator/organization_log_type.py +++ /dev/null @@ -1,52 +0,0 @@ -class OrganizationLogType: - ( - TYPE_SLACK_DEFAULT_CHANNEL_CHANGED, - TYPE_SLACK_WORKSPACE_CONNECTED, - TYPE_SLACK_WORKSPACE_DISCONNECTED, - TYPE_TELEGRAM_DEFAULT_CHANNEL_CHANGED, - TYPE_TELEGRAM_CHANNEL_CONNECTED, - TYPE_TELEGRAM_CHANNEL_DISCONNECTED, - TYPE_INTEGRATION_CREATED, - TYPE_INTEGRATION_DELETED, - TYPE_INTEGRATION_CHANGED, - TYPE_HEARTBEAT_CREATED, - TYPE_HEARTBEAT_CHANGED, - TYPE_CHANNEL_FILTER_CREATED, - TYPE_CHANNEL_FILTER_DELETED, - TYPE_CHANNEL_FILTER_CHANGED, - TYPE_ESCALATION_CHAIN_CREATED, - TYPE_ESCALATION_CHAIN_DELETED, - TYPE_ESCALATION_CHAIN_CHANGED, - TYPE_ESCALATION_STEP_CREATED, - TYPE_ESCALATION_STEP_DELETED, - TYPE_ESCALATION_STEP_CHANGED, - TYPE_MAINTENANCE_STARTED_FOR_ORGANIZATION, - TYPE_MAINTENANCE_STARTED_FOR_INTEGRATION, - TYPE_MAINTENANCE_STOPPED_FOR_ORGANIZATION, - TYPE_MAINTENANCE_STOPPED_FOR_INTEGRATION, - TYPE_MAINTENANCE_DEBUG_STARTED_FOR_ORGANIZATION, - TYPE_MAINTENANCE_DEBUG_STARTED_FOR_INTEGRATION, - TYPE_MAINTENANCE_DEBUG_STOPPED_FOR_ORGANIZATION, - TYPE_MAINTENANCE_DEBUG_STOPPED_FOR_INTEGRATION, - TYPE_CUSTOM_ACTION_CREATED, - TYPE_CUSTOM_ACTION_DELETED, - TYPE_CUSTOM_ACTION_CHANGED, - TYPE_SCHEDULE_CREATED, - TYPE_SCHEDULE_DELETED, - TYPE_SCHEDULE_CHANGED, - TYPE_ON_CALL_SHIFT_CREATED, - TYPE_ON_CALL_SHIFT_DELETED, - TYPE_ON_CALL_SHIFT_CHANGED, - TYPE_NEW_USER_ADDED, - TYPE_ORGANIZATION_SETTINGS_CHANGED, - TYPE_USER_SETTINGS_CHANGED, - TYPE_TELEGRAM_TO_USER_CONNECTED, - TYPE_TELEGRAM_FROM_USER_DISCONNECTED, - TYPE_API_TOKEN_CREATED, - TYPE_API_TOKEN_REVOKED, - TYPE_ESCALATION_CHAIN_COPIED, - TYPE_SCHEDULE_EXPORT_TOKEN_CREATED, - TYPE_MESSAGING_BACKEND_CHANNEL_CHANGED, - TYPE_MESSAGING_BACKEND_CHANNEL_DELETED, - TYPE_MESSAGING_BACKEND_USER_DISCONNECTED, - ) = range(49) diff --git a/engine/apps/user_management/tests/test_organization.py b/engine/apps/user_management/tests/test_organization.py index 83f342e6..9cfd06e7 100644 --- a/engine/apps/user_management/tests/test_organization.py +++ b/engine/apps/user_management/tests/test_organization.py @@ -24,7 +24,6 @@ def test_organization_delete( make_escalation_chain, make_escalation_policy, make_channel_filter, - make_organization_log_record, make_user_notification_policy, make_telegram_user_connector, make_telegram_channel, @@ -74,8 +73,6 @@ def test_organization_delete( alert_receive_channel = make_alert_receive_channel(organization=organization, author=user_1) channel_filter = make_channel_filter(alert_receive_channel, is_default=True, escalation_chain=escalation_chain) - organization_log_record = make_organization_log_record(organization=organization, user=user_1) - alert_group = make_alert_group( alert_receive_channel=alert_receive_channel, acknowledged_by_user=user_1, @@ -142,7 +139,6 @@ def test_organization_delete( escalation_policy, alert_receive_channel, channel_filter, - organization_log_record, alert_group, alert, alert_group_log_record, diff --git a/engine/common/insight_log/__init__.py b/engine/common/insight_log/__init__.py new file mode 100644 index 00000000..9bf46cf3 --- /dev/null +++ b/engine/common/insight_log/__init__.py @@ -0,0 +1,3 @@ +from .chatops_insight_logs import ChatOpsEvent, ChatOpsType, write_chatops_insight_log # noqa +from .maintenance_insight_log import MaintenanceEvent, write_maintenance_insight_log # noqa +from .resource_insight_logs import EntityEvent, write_resource_insight_log # noqa diff --git a/engine/common/insight_log/chatops_insight_logs.py b/engine/common/insight_log/chatops_insight_logs.py new file mode 100644 index 00000000..aae90ed3 --- /dev/null +++ b/engine/common/insight_log/chatops_insight_logs.py @@ -0,0 +1,45 @@ +import enum +import json +import logging + +from .insight_logs_enabled_check import is_insight_logs_enabled + +insight_logger = logging.getLogger("insight_logger") +logger = logging.getLogger(__name__) + + +class ChatOpsEvent(enum.Enum): + WORKSPACE_CONNECTED = "started" + WORKSPACE_DISCONNECTED = "finished" + CHANNEL_CONNECTED = "channel_connected" + CHANNEL_DISCONNECTED = "channel_disconnected" + USER_LINKED = "user_linked" + USER_UNLINKED = "used_unlinked" + DEFAULT_CHANNEL_CHANGED = "default_channel_changed" + + +class ChatOpsType(enum.Enum): + # Keep in sync with messaging backends' id. + # In perfect world backend_ids should be used intead of this enums + # It can be achieved when we move refactor slack and telegram to use the messaging_backend system. + SLACK = "SLACK" + MSTEAMS = "MSTEAMS" + TELEGRAM = "TELEGRAM" + + +def write_chatops_insight_log(author, event_name: ChatOpsEvent, chatops_type: ChatOpsType, **kwargs): + try: + organization = author.organization + + if is_insight_logs_enabled(organization): + tenant_id = organization.stack_id + 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 + for k, v in kwargs.items(): + log_line += f" {k}={json.dumps(v)}" + + insight_logger.info(log_line) + except Exception as e: + logger.warning(f"insight_log.failed_to_write_chatops_insight_log exception={e}") diff --git a/engine/common/insight_log/insight_logs_enabled_check.py b/engine/common/insight_log/insight_logs_enabled_check.py new file mode 100644 index 00000000..a9a373d6 --- /dev/null +++ b/engine/common/insight_log/insight_logs_enabled_check.py @@ -0,0 +1,15 @@ +from django.apps import apps + + +def is_insight_logs_enabled(organization): + """ + is_insight_logs_enabled checks if inside logs enabled for given organization. + """ + DynamicSetting = apps.get_model("base", "DynamicSetting") + org_id_to_enable_insight_logs, _ = DynamicSetting.objects.get_or_create( + name="org_id_to_enable_insight_logs", + defaults={"json_value": []}, + ) + log_all = "all" in org_id_to_enable_insight_logs.json_value + insight_logs_enabled = organization.id in org_id_to_enable_insight_logs.json_value + return log_all or insight_logs_enabled diff --git a/engine/common/insight_log/maintenance_insight_log.py b/engine/common/insight_log/maintenance_insight_log.py new file mode 100644 index 00000000..f1260351 --- /dev/null +++ b/engine/common/insight_log/maintenance_insight_log.py @@ -0,0 +1,38 @@ +import enum +import json +import logging + +from .insight_logs_enabled_check import is_insight_logs_enabled + +insight_logger = logging.getLogger("insight_logger") +logger = logging.getLogger(__name__) + + +class MaintenanceEvent(enum.Enum): + STARTED = "started" + FINISHED = "finished" + + +def write_maintenance_insight_log(instance, user, event: MaintenanceEvent): + try: + organization = instance.get_organization() + + tenant_id = organization.stack_id + team = instance.get_team() + entity_name = json.dumps(instance.insight_logs_verbal) + entity_id = instance.public_primary_key + maintenance_mode = instance.get_maintenance_mode_display() + + if is_insight_logs_enabled(organization): + log_line = f"tenant_id={tenant_id} action_type=maintenance action_name={event.value} maintenance_mode={maintenance_mode} resource_id={entity_id} resource_name={entity_name}" # noqa + if team: + log_line += f" team={json.dumps(team.name)} team_id={team.public_primary_key}" + else: + log_line += f' team="General"' + if user: + username = json.dumps(user.username) + user_id = user.public_primary_key + log_line += f" user_id={user_id} username={username} " + insight_logger.info(log_line) + except Exception as e: + logger.warning(f"insight_log.failed_to_write_maintenance_insight_log exception={e}") diff --git a/engine/common/insight_log/resource_insight_logs.py b/engine/common/insight_log/resource_insight_logs.py new file mode 100644 index 00000000..1b3a0ce9 --- /dev/null +++ b/engine/common/insight_log/resource_insight_logs.py @@ -0,0 +1,126 @@ +import enum +import json +import logging +import re +from abc import ABC, abstractmethod + +from .insight_logs_enabled_check import is_insight_logs_enabled + +insight_logger = logging.getLogger("insight_logger") +logger = logging.getLogger(__name__) + + +class EntityEvent(enum.Enum): + CREATED = "created" + UPDATED = "updated" + DELETED = "deleted" + + +class InsightLoggable(ABC): + @property + @abstractmethod + def public_primary_key(self): + pass + + @property + @abstractmethod + def insight_logs_verbal(self) -> str: + """ + insight_logs_verbal returns resource name for insight_log + """ + pass + + @property + @abstractmethod + def insight_logs_type_verbal(self) -> str: + """ + insight_logs_type_verbal resource type for insight_log + """ + pass + + @property + @abstractmethod + def insight_logs_serialized(self) -> dict: + """ + insight_logs_serialized returns resource, serialized for insight_log + """ + pass + + @property + @abstractmethod + def insight_logs_metadata(self) -> dict: + """ + insight_logs_metadata returns resource's fields which should be always present in the insight_log line even if + they weren't changed + """ + pass + + +def write_resource_insight_log(instance: InsightLoggable, author, event: EntityEvent, prev_state=None, new_state=None): + try: + organization = author.organization + if is_insight_logs_enabled(organization): + tenant_id = organization.stack_id + author_id = author.public_primary_key + author = json.dumps(author.username) + entity_type = instance.insight_logs_type_verbal + try: + entity_id = instance.public_primary_key + except AttributeError: + # Fallback for entities which have no public_primary_key, E.g. public api token, schedule export token + entity_id = instance.id + entity_name = json.dumps(instance.insight_logs_verbal) + metadata = instance.insight_logs_metadata + log_line = f"tenant_id={tenant_id} author_id={author_id} author={author} action_type=resource action={event.value} resource_type={entity_type} resource_id={entity_id} resource_name={entity_name}" # noqa + for k, v in metadata.items(): + log_line += f" {k}={json.dumps(v)}" + if prev_state and new_state: + prev_state, new_state = state_diff_finder(prev_state, new_state) + prev_state = escape_json_str_for_insight_log(json.dumps(format_state_for_insight_log(prev_state))) + new_state = escape_json_str_for_insight_log(json.dumps(format_state_for_insight_log(new_state))) + log_line += f' prev_state="{prev_state}"' + log_line += f' new_state="{new_state}"' + insight_logger.info(log_line) + except Exception as e: + logger.warning(f"insight_log.failed_to_write_entity_insight_log exception={e}") + + +def state_diff_finder(prev_state: dict, new_state: dict): + """ + state_diff_finder returns diff between two serialized representations of the resource + """ + before_diff = {} + after_diff = {} + for k, v in prev_state.items(): + if k not in new_state: + before_diff[k] = v + continue + if new_state[k] != v: + before_diff[k] = prev_state[k] + after_diff[k] = new_state[k] + for k, v in new_state.items(): + if k not in prev_state: + after_diff[k] = v + return before_diff, after_diff + + +def escape_json_str_for_insight_log(string): + """ + escape_json_str escapes double quotes near keys and values in json string + """ + return re.sub(r"(?