From 58d30e7f85ad09711dc6cca0b5ed8b202f9a7929 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Tue, 6 Feb 2024 18:28:31 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E8=AE=B0=E5=BD=95=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E6=B4=BB=E5=8A=A8=E6=97=A5=E5=BF=97=20(#12523)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: 更新会话生命周期日志 * perf: 优化错误原因 * perf: 增加错误类型 --------- Co-authored-by: Eric --- apps/terminal/api/session/session.py | 22 ++- apps/terminal/serializers/session.py | 8 + apps/terminal/session_lifecycle.py | 176 ++++++++++++++++++ .../signal_handlers/session_sharing.py | 5 + 4 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 apps/terminal/session_lifecycle.py diff --git a/apps/terminal/api/session/session.py b/apps/terminal/api/session/session.py index 8865801ac..400735f53 100644 --- a/apps/terminal/api/session/session.py +++ b/apps/terminal/api/session/session.py @@ -18,10 +18,11 @@ from rest_framework.response import Response from audits.const import ActionChoices from common.api import AsyncApiMixin -from common.const.http import GET +from common.const.http import GET, POST from common.drf.filters import BaseFilterSet from common.drf.filters import DatetimeRangeFilterBackend from common.drf.renders import PassthroughRenderer +from common.permissions import IsServiceAccount from common.storage.replay import ReplayStorageHandler from common.utils import data_to_json, is_uuid, i18n_fmt from common.utils import get_logger, get_object_or_none @@ -33,6 +34,7 @@ from terminal import serializers from terminal.const import TerminalType from terminal.models import Session from terminal.permissions import IsSessionAssignee +from terminal.session_lifecycle import lifecycle_events_map, reasons_map from terminal.utils import is_session_approver from users.models import User @@ -79,6 +81,7 @@ class SessionViewSet(RecordViewLogMixin, OrgBulkModelViewSet): serializer_classes = { 'default': serializers.SessionSerializer, 'display': serializers.SessionDisplaySerializer, + 'lifecycle_log': serializers.SessionLifecycleLogSerializer, } search_fields = [ "user", "asset", "account", "remote_addr", @@ -168,6 +171,23 @@ class SessionViewSet(RecordViewLogMixin, OrgBulkModelViewSet): count = queryset.count() return Response({'count': count}) + @action(methods=[POST], detail=True, permission_classes=[IsServiceAccount], url_path='lifecycle_log', + url_name='lifecycle_log') + def lifecycle_log(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + validated_data = serializer.validated_data + event = validated_data.pop('event', None) + event_class = lifecycle_events_map.get(event, None) + if not event_class: + return Response({'msg': f'event_name {event} invalid'}, status=400) + session = self.get_object() + reason = validated_data.pop('reason', None) + reason = reasons_map.get(reason, reason) + event_obj = event_class(session, reason, **validated_data) + activity_log = event_obj.create_activity_log() + return Response({'msg': 'ok', 'id': activity_log.id}) + def get_queryset(self): queryset = super().get_queryset() \ .prefetch_related('terminal') \ diff --git a/apps/terminal/serializers/session.py b/apps/terminal/serializers/session.py index ba8ecfdf8..74c87dfb0 100644 --- a/apps/terminal/serializers/session.py +++ b/apps/terminal/serializers/session.py @@ -4,6 +4,7 @@ from rest_framework import serializers from common.serializers.fields import LabeledChoiceField from common.utils import pretty_string from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from terminal.session_lifecycle import lifecycle_events_map from .terminal import TerminalSmallSerializer from ..const import SessionType, SessionErrorReason from ..models import Session @@ -11,6 +12,7 @@ from ..models import Session __all__ = [ 'SessionSerializer', 'SessionDisplaySerializer', 'ReplaySerializer', 'SessionJoinValidateSerializer', + 'SessionLifecycleLogSerializer' ] @@ -77,3 +79,9 @@ class ReplaySerializer(serializers.Serializer): class SessionJoinValidateSerializer(serializers.Serializer): user_id = serializers.UUIDField() session_id = serializers.UUIDField() + + +class SessionLifecycleLogSerializer(serializers.Serializer): + event = serializers.ChoiceField(choices=list(lifecycle_events_map.keys())) + reason = serializers.CharField(required=False) + user = serializers.CharField(required=False) diff --git a/apps/terminal/session_lifecycle.py b/apps/terminal/session_lifecycle.py new file mode 100644 index 000000000..3de63d676 --- /dev/null +++ b/apps/terminal/session_lifecycle.py @@ -0,0 +1,176 @@ +from django.utils.translation import gettext_noop + +from audits.const import ActivityChoices +from audits.models import ActivityLog +from common.utils import i18n_fmt +from terminal.models import Session + + +class SessionLifecycleEventBase(object): + + def __init__(self, session: Session, reason, *args, **kwargs): + self.session = session + self.reason = reason + + def detail(self): + raise NotImplementedError + + def create_activity_log(self): + log_obj = ActivityLog.objects.create( + resource_id=self.session.id, + type=ActivityChoices.session_log, + detail=self.detail(), + org_id=self.session.org_id + ) + return log_obj + + +class AssetConnectSuccess(SessionLifecycleEventBase): + name = "asset_connect_success" + i18n_text = gettext_noop("Connect to asset %s success") + + def detail(self): + return i18n_fmt(self.i18n_text, self.session.asset) + + +class AssetConnectFinished(SessionLifecycleEventBase): + name = "asset_connect_finished" + i18n_text = gettext_noop("Connect to asset %s finished: %s") + + def detail(self): + asset = self.session.asset + reason = self.reason + return i18n_fmt(self.i18n_text, asset, reason) + + +class UserCreateShareLink(SessionLifecycleEventBase): + name = "create_share_link" + i18n_text = gettext_noop("User %s create share link") + + def detail(self): + user = self.session.user + return i18n_fmt(self.i18n_text, user) + + +class UserJoinSession(SessionLifecycleEventBase): + name = "user_join_session" + i18n_text = gettext_noop("User %s join session") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.user = kwargs.get("user") + + def detail(self): + return i18n_fmt(self.i18n_text, self.user) + + +class UserLeaveSession(SessionLifecycleEventBase): + name = "user_leave_session" + i18n_text = gettext_noop("User %s leave session") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.user = kwargs.get("user") + + def detail(self): + return i18n_fmt(self.i18n_text, self.user) + + +class AdminJoinMonitor(SessionLifecycleEventBase): + name = "admin_join_monitor" + i18n_text = gettext_noop("User %s join to monitor session") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.user = kwargs.get("user") + + def detail(self): + return i18n_fmt(self.i18n_text, self.user) + + +class AdminExitMonitor(SessionLifecycleEventBase): + name = "admin_exit_monitor" + i18n_text = gettext_noop("User %s exit to monitor session") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.user = kwargs.get("user") + + def detail(self): + return i18n_fmt(self.i18n_text, self.user) + + +class ReplayConvertStart(SessionLifecycleEventBase): + name = "replay_convert_start" + i18n_text = gettext_noop("Replay start to convert") + + def detail(self): + return self.i18n_text + + +class ReplayConvertSuccess(SessionLifecycleEventBase): + name = "replay_convert_success" + i18n_text = gettext_noop("Replay successfully converted to MP4 format") + + def detail(self): + return self.i18n_text + + +class ReplayConvertFailure(SessionLifecycleEventBase): + name = "replay_convert_failure" + i18n_text = gettext_noop("Replay failed to convert to MP4 format: %s") + + def detail(self): + return i18n_fmt(self.i18n_text, self.reason) + + +class ReplayUploadStart(SessionLifecycleEventBase): + name = "replay_upload_start" + i18n_text = gettext_noop("Replay start to upload") + + def detail(self): + return self.i18n_text + + +class ReplayUploadSuccess(SessionLifecycleEventBase): + name = "replay_upload_success" + i18n_text = gettext_noop("Replay successfully uploaded") + + def detail(self): + return self.i18n_text + + +class ReplayUploadFailure(SessionLifecycleEventBase): + name = "replay_upload_failure" + i18n_text = gettext_noop("Replay failed to upload: %s") + + def detail(self): + return i18n_fmt(self.i18n_text, self.reason) + + +reasons_map = { + 'connect_failed': gettext_noop('connect failed'), + 'connect_disconnect': gettext_noop('connection disconnect'), + 'user_close': gettext_noop('user closed'), + 'idle_disconnect': gettext_noop('idle disconnect'), + 'admin_terminate': gettext_noop('admin terminated'), + 'max_session_timeout': gettext_noop('maximum session time has been reached'), + 'permission_expired': gettext_noop('permission has expired'), + 'null_storage': gettext_noop('storage is null'), +} + +lifecycle_events_map = { + AssetConnectSuccess.name: AssetConnectSuccess, + AssetConnectFinished.name: AssetConnectFinished, + UserCreateShareLink.name: UserCreateShareLink, + UserJoinSession.name: UserJoinSession, + UserLeaveSession.name: UserLeaveSession, + AdminJoinMonitor.name: AdminJoinMonitor, + AdminExitMonitor.name: AdminExitMonitor, + ReplayConvertStart.name: ReplayConvertStart, + ReplayConvertSuccess.name: ReplayConvertSuccess, + ReplayConvertFailure.name: ReplayConvertFailure, + ReplayUploadStart.name: ReplayUploadStart, + ReplayUploadSuccess.name: ReplayUploadSuccess, + ReplayUploadFailure.name: ReplayUploadFailure, +} diff --git a/apps/terminal/signal_handlers/session_sharing.py b/apps/terminal/signal_handlers/session_sharing.py index f20ccf665..6af1f40e8 100644 --- a/apps/terminal/signal_handlers/session_sharing.py +++ b/apps/terminal/signal_handlers/session_sharing.py @@ -3,6 +3,7 @@ from django.dispatch import receiver from terminal.models import SessionSharing from terminal.notifications import SessionSharingMessage +from terminal.session_lifecycle import UserCreateShareLink @receiver(post_save, sender=SessionSharing) @@ -11,3 +12,7 @@ def on_session_sharing_created(sender, instance: SessionSharing, created, **kwar return for user in instance.users_queryset: SessionSharingMessage(user, instance).publish_async() + + # 创建会话分享活动日志 + session = instance.session + UserCreateShareLink(session, None).create_activity_log()