perf: asset activity

pull/15630/head
feng 2025-07-24 22:39:47 +08:00
parent a3b0439be5
commit 9631dec210
12 changed files with 159 additions and 52 deletions

View File

@ -90,10 +90,10 @@ class ChangeSecretDashboardApi(APIView):
def get_change_secret_asset_queryset(self):
qs = self.change_secrets_queryset
node_ids = qs.filter(nodes__isnull=False).values_list('nodes', flat=True).distinct()
nodes = Node.objects.filter(id__in=node_ids)
node_ids = qs.values_list('nodes', flat=True).distinct()
nodes = Node.objects.filter(id__in=node_ids).only('id', 'key')
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
direct_asset_ids = qs.filter(assets__isnull=False).values_list('assets', flat=True).distinct()
direct_asset_ids = qs.values_list('assets', flat=True).distinct()
asset_ids = set(list(direct_asset_ids) + list(node_asset_ids))
return Asset.objects.filter(id__in=asset_ids)

View File

@ -20,3 +20,7 @@ class Category(ChoicesMixin, models.TextChoices):
_category = getattr(cls, category.upper(), None)
choices = [(_category.value, _category.label)] if _category else cls.choices
return choices
@classmethod
def as_dict(cls):
return {choice.value: choice.label for choice in cls}

View File

@ -53,7 +53,7 @@ class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
return name
def get_all_assets(self):
nodes = self.nodes.all()
nodes = self.nodes.only("id", "key")
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list("id", flat=True)
direct_asset_ids = self.assets.all().values_list("id", flat=True)
asset_ids = set(list(direct_asset_ids) + list(node_asset_ids))

View File

@ -1,2 +1,3 @@
from .assets import *
from .report import *
from .users import *

View File

@ -1 +1,2 @@
from .activity import *
from .asset import *

View File

@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
#
from collections import defaultdict
from django.db.models import Count, Q
from django.http.response import JsonResponse
from rest_framework.views import APIView
from assets.const import AllTypes
from assets.models import Asset
from common.permissions import IsValidLicense
from common.utils import lazyproperty
from rbac.permissions import RBACPermission
from reports.api.assets.base import group_stats
from reports.mixins import DateRangeMixin
from terminal.const import LoginFrom
from terminal.models import Session
__all__ = ['AssetActivityApi']
class AssetActivityApi(DateRangeMixin, APIView):
http_method_names = ['get']
# TODO: Define the required RBAC permissions for this API
rbac_perms = {
'GET': 'terminal.view_session',
}
permission_classes = [RBACPermission, IsValidLicense]
def get_asset_login_metrics(self, queryset):
data = defaultdict(set)
for t, asset in queryset.values_list('date_start', 'asset'):
date_str = str(t.date())
data[date_str].add(asset)
metrics = [len(data.get(str(d), set())) for d in self.date_range_list]
return metrics
@lazyproperty
def session_qs(self):
return Session.objects.all()
def get(self, request, *args, **kwargs):
qs = self.session_qs
qs = self.filter_by_date_range(qs, 'date_start')
all_type_dict = dict(AllTypes.choices())
stats = qs.aggregate(
total=Count(1),
asset_online=Count(1, filter=Q(is_finished=False)),
asset_count=Count('asset_id', distinct=True),
user_count=Count('user_id', distinct=True),
is_success_count=Count(1, filter=Q(is_success=True)),
)
asset_ids = {str(_id) for _id in qs.values_list('asset_id', flat=True).distinct()}
assets = Asset.objects.filter(id__in=asset_ids)
asset_login_by_protocol = group_stats(
qs, 'protocol_label', 'protocol'
)
asset_login_by_from = group_stats(
qs, 'login_from_label', 'login_from', LoginFrom.as_dict()
)
asset_by_type = group_stats(
assets, 'type', 'platform__type', all_type_dict,
)
dates_metrics_date = [date.strftime('%m-%d') for date in self.date_range_list] or ['0']
payload = {
**stats,
'asset_login_by_type': asset_by_type,
'asset_login_by_from': asset_login_by_from,
'asset_login_by_protocol': asset_login_by_protocol,
'asset_login_log_metrics': {
'dates_metrics_date': dates_metrics_date,
'dates_metrics_total': self.get_asset_login_metrics(qs),
}
}
return JsonResponse(payload, status=200)

View File

@ -1,16 +1,17 @@
# -*- coding: utf-8 -*-
#
from django.db.models import Count, Q, F
from django.db.models import Count, Q
from django.http import JsonResponse
from django.utils import timezone
from rest_framework.views import APIView
from assets.const import AllTypes, Connectivity, Category
from assets.models import Asset
from assets.models import Asset, Platform
from common.permissions import IsValidLicense
from common.utils import lazyproperty
from rbac.permissions import RBACPermission
from reports.api.assets.base import group_stats
__all__ = ['AssetStatisticApi']
@ -26,56 +27,48 @@ class AssetStatisticApi(APIView):
@lazyproperty
def base_qs(self):
return Asset.objects.only(
'id', 'platform', 'zone', 'connectivity', 'created_time'
'id', 'platform', 'zone', 'connectivity', 'is_active'
)
@staticmethod
def _group_stats(queryset, alias, key, label_map=None):
grouped = (
queryset
.values(**{alias: F(key)})
.annotate(total=Count('id'))
)
data = [
{
alias: val,
'total': cnt,
**({'label': label_map.get(val, val)} if label_map else {})
}
for val, cnt in grouped.values_list(alias, 'total')
]
return data
def get(self, request, *args, **kwargs):
qs = self.base_qs
all_type_dict = dict(AllTypes.choices())
platform_by_type = group_stats(
Platform.objects.all(), 'type_label', 'type', all_type_dict,
)
stats = qs.aggregate(
total=Count('id'),
active=Count('id', filter=Q(is_active=True)),
connected=Count('id', filter=Q(connectivity=Connectivity.OK)),
total=Count(1),
active=Count(1, filter=Q(is_active=True)),
connected=Count(1, filter=Q(connectivity=Connectivity.OK)),
)
by_type = self._group_stats(
qs, 'type', 'platform__type', dict(AllTypes.choices()),
by_type = group_stats(
qs, 'type', 'platform__type', all_type_dict,
)
by_category = self._group_stats(
qs, 'category', 'platform__category', dict(Category.choices())
by_category = group_stats(
qs, 'category', 'platform__category', Category.as_dict()
)
by_zone = self._group_stats(
qs, 'zone', 'zone__name'
by_zone = group_stats(
qs, 'zone_label', 'zone__name'
)
week_start = timezone.now() + timezone.timedelta(days=7)
assets_added_this_week = qs.filter(date_created__gte=week_start).count()
assets_added_this_week_qs = qs.filter(date_created__gte=week_start)
assets_added_this_week_by_type = group_stats(
assets_added_this_week_qs, 'type', 'platform__type', all_type_dict,
)
payload = {
**stats,
'assets_by_platform_type': by_type,
'platform_by_type': platform_by_type,
'assets_by_type': by_type,
'assets_by_category': by_category,
'assets_by_zone': by_zone,
'assets_added_this_week': assets_added_this_week,
'assets_added_this_week_count': assets_added_this_week_qs.count(),
'assets_added_this_week_by_type': assets_added_this_week_by_type,
}
return JsonResponse(payload, status=200)

View File

@ -0,0 +1,21 @@
from django.db.models import Count, F
def group_stats(queryset, alias, key, label_map=None):
grouped = (
queryset
.exclude(**{f'{key}__isnull': True})
.values(**{alias: F(key)})
.annotate(total=Count('id'))
)
data = [
{
alias: val,
'total': cnt,
**({'label': label_map.get(val, val)} if label_map else {})
}
for val, cnt in grouped.values_list(alias, 'total')
]
return data

View File

@ -7,5 +7,7 @@ app_name = 'reports'
urlpatterns = [
path('reports/', api.ReportViewSet.as_view(), name='report-list'),
path('reports/users/', api.UserReportApi.as_view(), name='user-list'),
path('reports/user-change-password/', api.UserChangeSecretApi.as_view(), name='user-change-password')
path('reports/user-change-password/', api.UserChangeSecretApi.as_view(), name='user-change-password'),
path('reports/asset-statistic/', api.AssetStatisticApi.as_view(), name='asset-statistic'),
path('reports/asset-activity/', api.AssetActivityApi.as_view(), name='asset-activity')
]

View File

@ -106,3 +106,14 @@ class SessionErrorReason(TextChoices):
replay_upload_failed = 'replay_upload_failed', _('Replay upload failed')
replay_convert_failed = 'replay_convert_failed', _('Replay convert failed')
replay_unsupported = 'replay_unsupported', _('Replay unsupported')
class LoginFrom(TextChoices):
ST = 'ST', 'SSH Terminal'
RT = 'RT', 'RDP Terminal'
WT = 'WT', 'Web Terminal'
DT = 'DT', 'DB Terminal'
@classmethod
def as_dict(cls):
return {choice.value: choice.label for choice in cls}

View File

@ -17,17 +17,11 @@ from common.const.signals import OP_LOG_SKIP_SIGNAL
from common.utils import get_object_or_none, lazyproperty
from orgs.mixins.models import OrgModelMixin
from terminal.backends import get_multi_command_storage
from terminal.const import SessionType, TerminalType
from terminal.const import SessionType, TerminalType, LoginFrom
from users.models import User
class Session(OrgModelMixin):
class LOGIN_FROM(models.TextChoices):
ST = 'ST', 'SSH Terminal'
RT = 'RT', 'RDP Terminal'
WT = 'WT', 'Web Terminal'
DT = 'DT', 'DB Terminal'
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
user = models.CharField(max_length=128, verbose_name=_("User"), db_index=True)
user_id = models.CharField(blank=True, default='', max_length=36, db_index=True)
@ -36,7 +30,7 @@ class Session(OrgModelMixin):
account = models.CharField(max_length=128, verbose_name=_("Account"), db_index=True)
account_id = models.CharField(max_length=128, verbose_name=_("Account ID"), db_index=True)
protocol = models.CharField(default='ssh', max_length=16, db_index=True)
login_from = models.CharField(max_length=2, choices=LOGIN_FROM.choices, default="ST", verbose_name=_("Login from"))
login_from = models.CharField(max_length=2, choices=LoginFrom.choices, default="ST", verbose_name=_("Login from"))
type = models.CharField(max_length=16, default='normal', db_index=True)
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
is_success = models.BooleanField(default=True, db_index=True)

View File

@ -2,15 +2,15 @@ import datetime
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from common.db.models import JMSBaseModel
from common.utils import is_uuid
from orgs.mixins.models import OrgModelMixin
from orgs.utils import tmp_to_root_org
from terminal.const import LoginFrom
from users.models import User
from .session import Session
__all__ = ['SessionSharing', 'SessionJoinRecord']
@ -89,8 +89,6 @@ class SessionSharing(JMSBaseModel, OrgModelMixin):
class SessionJoinRecord(JMSBaseModel, OrgModelMixin):
LOGIN_FROM = Session.LOGIN_FROM
session = models.ForeignKey(
'terminal.Session', on_delete=models.CASCADE, verbose_name=_('Session')
)
@ -114,7 +112,7 @@ class SessionJoinRecord(JMSBaseModel, OrgModelMixin):
db_index=True
)
login_from = models.CharField(
max_length=2, choices=LOGIN_FROM.choices, default="WT",
max_length=2, choices=LoginFrom.choices, default="WT",
verbose_name=_("Login from")
)
is_success = models.BooleanField(