perf: 记录会话活动日志 (#12523)

* perf: 更新会话生命周期日志

* perf: 优化错误原因

* perf: 增加错误类型

---------

Co-authored-by: Eric <xplzv@126.com>
pull/12660/head
fit2bot 2024-02-06 18:28:31 +08:00 committed by GitHub
parent 2062778ab8
commit 58d30e7f85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 210 additions and 1 deletions

View File

@ -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') \

View File

@ -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)

View File

@ -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,
}

View File

@ -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()