perf: 修改terminal statuts

perf: 优化status api

perf: 优化 status api

perf: 修改sesion参数

perf: 修改migrations

perf: 优化数据结构

perf: 修改保留日志

perf: 优化之前的一个写法
pull/5953/head^2
ibuler 2021-03-26 19:09:34 +08:00 committed by xinwen
parent a5179d1596
commit 9cd5675209
15 changed files with 380 additions and 306 deletions

View File

@ -5,4 +5,4 @@ from .session import *
from .command import * from .command import *
from .task import * from .task import *
from .storage import * from .storage import *
from .component import * from .status import *

View File

@ -1,34 +0,0 @@
# -*- coding: utf-8 -*-
#
import logging
from rest_framework import generics, status
from rest_framework.views import Response
from .. import serializers
from ..utils import ComponentsMetricsUtil
from common.permissions import IsAppUser, IsSuperUser
logger = logging.getLogger(__file__)
__all__ = [
'ComponentsStateAPIView', 'ComponentsMetricsAPIView',
]
class ComponentsStateAPIView(generics.CreateAPIView):
""" koko, guacamole, omnidb 上报状态 """
permission_classes = (IsAppUser,)
serializer_class = serializers.ComponentsStateSerializer
class ComponentsMetricsAPIView(generics.GenericAPIView):
""" 返回汇总组件指标数据 """
permission_classes = (IsSuperUser,)
def get(self, request, *args, **kwargs):
tp = request.query_params.get('type')
util = ComponentsMetricsUtil()
metrics = util.get_metrics(tp)
return Response(metrics, status=status.HTTP_200_OK)

View File

@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
#
import logging
from django.shortcuts import get_object_or_404
from rest_framework import viewsets, generics
from rest_framework.views import Response
from rest_framework import status
from common.permissions import IsAppUser, IsOrgAdminOrAppUser, IsSuperUser
from ..models import Terminal, Status, Session
from .. import serializers
from ..utils import TypedComponentsStatusMetricsUtil
logger = logging.getLogger(__file__)
__all__ = [
'StatusViewSet',
'ComponentsMetricsAPIView',
]
class StatusViewSet(viewsets.ModelViewSet):
queryset = Status.objects.all()
serializer_class = serializers.StatusSerializer
permission_classes = (IsOrgAdminOrAppUser,)
session_serializer_class = serializers.SessionSerializer
task_serializer_class = serializers.TaskSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.handle_sessions()
self.perform_create(serializer)
tasks = self.request.user.terminal.task_set.filter(is_finished=False)
serializer = self.task_serializer_class(tasks, many=True)
return Response(serializer.data, status=201)
def handle_sessions(self):
session_ids = self.request.data.get('sessions', [])
# guacamole 上报的 session 是字符串
# "[53cd3e47-210f-41d8-b3c6-a184f3, 53cd3e47-210f-41d8-b3c6-a184f4]"
if isinstance(session_ids, str):
session_ids = session_ids[1:-1].split(',')
session_ids = [sid.strip() for sid in session_ids if sid.strip()]
Session.set_sessions_active(session_ids)
def get_queryset(self):
terminal_id = self.kwargs.get("terminal", None)
if terminal_id:
terminal = get_object_or_404(Terminal, id=terminal_id)
return terminal.status_set.all()
return super().get_queryset()
def perform_create(self, serializer):
serializer.validated_data.pop('sessions', None)
serializer.validated_data["terminal"] = self.request.user.terminal
return super().perform_create(serializer)
def get_permissions(self):
if self.action == "create":
self.permission_classes = (IsAppUser,)
return super().get_permissions()
class ComponentsMetricsAPIView(generics.GenericAPIView):
""" 返回汇总组件指标数据 """
permission_classes = (IsSuperUser,)
def get(self, request, *args, **kwargs):
util = TypedComponentsStatusMetricsUtil()
metrics = util.get_metrics()
return Response(metrics, status=status.HTTP_200_OK)

View File

@ -4,8 +4,7 @@ import logging
import uuid import uuid
from django.core.cache import cache from django.core.cache import cache
from django.shortcuts import get_object_or_404 from rest_framework import generics
from rest_framework import viewsets, generics
from rest_framework.views import APIView, Response from rest_framework.views import APIView, Response
from rest_framework import status from rest_framework import status
from django.conf import settings from django.conf import settings
@ -13,13 +12,13 @@ from django.conf import settings
from common.drf.api import JMSBulkModelViewSet from common.drf.api import JMSBulkModelViewSet
from common.utils import get_object_or_none from common.utils import get_object_or_none
from common.permissions import IsAppUser, IsOrgAdminOrAppUser, IsSuperUser, WithBootstrapToken from common.permissions import IsAppUser, IsSuperUser, WithBootstrapToken
from ..models import Terminal, Status, Session from ..models import Terminal
from .. import serializers from .. import serializers
from .. import exceptions from .. import exceptions
__all__ = [ __all__ = [
'TerminalViewSet', 'StatusViewSet', 'TerminalConfig', 'TerminalViewSet', 'TerminalConfig',
'TerminalRegistrationApi', 'TerminalRegistrationApi',
] ]
logger = logging.getLogger(__file__) logger = logging.getLogger(__file__)
@ -72,45 +71,6 @@ class TerminalViewSet(JMSBulkModelViewSet):
return queryset return queryset
class StatusViewSet(viewsets.ModelViewSet):
queryset = Status.objects.all()
serializer_class = serializers.StatusSerializer
permission_classes = (IsOrgAdminOrAppUser,)
session_serializer_class = serializers.SessionSerializer
task_serializer_class = serializers.TaskSerializer
def create(self, request, *args, **kwargs):
self.handle_sessions()
tasks = self.request.user.terminal.task_set.filter(is_finished=False)
serializer = self.task_serializer_class(tasks, many=True)
return Response(serializer.data, status=201)
def handle_sessions(self):
session_ids = self.request.data.get('sessions', [])
# guacamole 上报的 session 是字符串
# "[53cd3e47-210f-41d8-b3c6-a184f3, 53cd3e47-210f-41d8-b3c6-a184f4]"
if isinstance(session_ids, str):
session_ids = session_ids[1:-1].split(',')
session_ids = [sid.strip() for sid in session_ids if sid.strip()]
Session.set_sessions_active(session_ids)
def get_queryset(self):
terminal_id = self.kwargs.get("terminal", None)
if terminal_id:
terminal = get_object_or_404(Terminal, id=terminal_id)
self.queryset = terminal.status_set.all()
return self.queryset
def perform_create(self, serializer):
serializer.validated_data["terminal"] = self.request.user.terminal
return super().perform_create(serializer)
def get_permissions(self):
if self.action == "create":
self.permission_classes = (IsAppUser,)
return super().get_permissions()
class TerminalConfig(APIView): class TerminalConfig(APIView):
permission_classes = (IsAppUser,) permission_classes = (IsAppUser,)

View File

@ -31,6 +31,7 @@ class ComponentStatusChoices(TextChoices):
critical = 'critical', _('Critical') critical = 'critical', _('Critical')
high = 'high', _('High') high = 'high', _('High')
normal = 'normal', _('Normal') normal = 'normal', _('Normal')
offline = 'offline', _('Offline')
@classmethod @classmethod
def status(cls): def status(cls):

View File

@ -0,0 +1,43 @@
# Generated by Django 3.1 on 2021-03-29 09:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0032_auto_20210302_1853'),
]
operations = [
migrations.RenameField(
model_name='status',
old_name='cpu_used',
new_name='cpu_load',
),
migrations.AlterField(
model_name='status',
name='cpu_load',
field=models.FloatField(default=0, verbose_name='CPU Load'),
),
migrations.AddField(
model_name='status',
name='disk_used',
field=models.FloatField(default=0, verbose_name='Disk Used'),
),
migrations.AlterField(
model_name='status',
name='boot_time',
field=models.FloatField(default=0, verbose_name='Boot Time'),
),
migrations.AlterField(
model_name='status',
name='connections',
field=models.IntegerField(default=0, verbose_name='Connections'),
),
migrations.AlterField(
model_name='status',
name='threads',
field=models.IntegerField(default=0, verbose_name='Threads'),
),
]

View File

@ -14,7 +14,6 @@ from assets.models import Asset
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from common.db.models import ChoiceSet from common.db.models import ChoiceSet
from ..backends import get_multi_command_storage from ..backends import get_multi_command_storage
from .terminal import Terminal
class Session(OrgModelMixin): class Session(OrgModelMixin):
@ -47,7 +46,7 @@ class Session(OrgModelMixin):
is_finished = models.BooleanField(default=False, db_index=True) is_finished = models.BooleanField(default=False, db_index=True)
has_replay = models.BooleanField(default=False, verbose_name=_("Replay")) has_replay = models.BooleanField(default=False, verbose_name=_("Replay"))
has_command = models.BooleanField(default=False, verbose_name=_("Command")) has_command = models.BooleanField(default=False, verbose_name=_("Command"))
terminal = models.ForeignKey(Terminal, null=True, on_delete=models.DO_NOTHING, db_constraint=False) terminal = models.ForeignKey('terminal.Terminal', null=True, on_delete=models.DO_NOTHING, db_constraint=False)
protocol = models.CharField(choices=PROTOCOL.choices, default='ssh', max_length=16, db_index=True) protocol = models.CharField(choices=PROTOCOL.choices, default='ssh', max_length=16, db_index=True)
date_start = models.DateTimeField(verbose_name=_("Date start"), db_index=True, default=timezone.now) date_start = models.DateTimeField(verbose_name=_("Date start"), db_index=True, default=timezone.now)
date_end = models.DateTimeField(verbose_name=_("Date end"), null=True) date_end = models.DateTimeField(verbose_name=_("Date end"), null=True)

View File

@ -3,26 +3,62 @@ from __future__ import unicode_literals
import uuid import uuid
from django.db import models from django.db import models
from django.forms.models import model_to_dict
from django.core.cache import cache
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from .terminal import Terminal from common.utils import get_logger
logger = get_logger(__name__)
class Status(models.Model): class Status(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
session_online = models.IntegerField(verbose_name=_("Session Online"), default=0) session_online = models.IntegerField(verbose_name=_("Session Online"), default=0)
cpu_used = models.FloatField(verbose_name=_("CPU Usage")) cpu_load = models.FloatField(verbose_name=_("CPU Load"), default=0)
memory_used = models.FloatField(verbose_name=_("Memory Used")) memory_used = models.FloatField(verbose_name=_("Memory Used"))
connections = models.IntegerField(verbose_name=_("Connections")) disk_used = models.FloatField(verbose_name=_("Disk Used"), default=0)
threads = models.IntegerField(verbose_name=_("Threads")) connections = models.IntegerField(verbose_name=_("Connections"), default=0)
boot_time = models.FloatField(verbose_name=_("Boot Time")) threads = models.IntegerField(verbose_name=_("Threads"), default=0)
terminal = models.ForeignKey(Terminal, null=True, on_delete=models.CASCADE) boot_time = models.FloatField(verbose_name=_("Boot Time"), default=0)
terminal = models.ForeignKey('terminal.Terminal', null=True, on_delete=models.CASCADE)
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True)
CACHE_KEY = 'TERMINAL_STATUS_{}'
class Meta: class Meta:
db_table = 'terminal_status' db_table = 'terminal_status'
get_latest_by = 'date_created' get_latest_by = 'date_created'
def __str__(self): def save_to_cache(self):
return self.date_created.strftime("%Y-%m-%d %H:%M:%S") if not self.terminal:
return
key = self.CACHE_KEY.format(self.terminal.id)
data = model_to_dict(self)
cache.set(key, data, 60*3)
return data
@classmethod
def get_terminal_latest_status(cls, terminal):
from ..utils import ComputeStatUtil
stat = cls.get_terminal_latest_stat(terminal)
return ComputeStatUtil.compute_component_status(stat)
@classmethod
def get_terminal_latest_stat(cls, terminal):
key = cls.CACHE_KEY.format(terminal.id)
data = cache.get(key)
if not data:
return None
data.pop('terminal', None)
stat = cls(**data)
stat.terminal = terminal
return stat
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
self.terminal.set_alive(ttl=120)
return self.save_to_cache()
# return super().save()

View File

@ -1,168 +1,63 @@
from __future__ import unicode_literals
import uuid import uuid
from django.db import models from django.db import models
from django.core.cache import cache
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from common.utils import get_logger from common.utils import get_logger
from users.models import User from users.models import User
from .status import Status
from .. import const from .. import const
from ..const import ComponentStatusChoices as StatusChoice
from .session import Session
logger = get_logger(__file__) logger = get_logger(__file__)
class ComputeStatusMixin: class TerminalStatusMixin:
ALIVE_KEY = 'TERMINAL_ALIVE_{}'
# system status id: str
@staticmethod
def _common_compute_system_status(value, thresholds):
if thresholds[0] <= value <= thresholds[1]:
return const.ComponentStatusChoices.normal.value
elif thresholds[1] < value <= thresholds[2]:
return const.ComponentStatusChoices.high.value
else:
return const.ComponentStatusChoices.critical.value
def _compute_system_cpu_load_1_status(self, value):
thresholds = [0, 5, 20]
return self._common_compute_system_status(value, thresholds)
def _compute_system_memory_used_percent_status(self, value):
thresholds = [0, 85, 95]
return self._common_compute_system_status(value, thresholds)
def _compute_system_disk_used_percent_status(self, value):
thresholds = [0, 80, 99]
return self._common_compute_system_status(value, thresholds)
def _compute_system_status(self, state):
system_status_keys = [
'system_cpu_load_1', 'system_memory_used_percent', 'system_disk_used_percent'
]
system_status = []
for system_status_key in system_status_keys:
state_value = state.get(system_status_key)
if state_value is None:
msg = 'state: {}, state_key: {}, state_value: {}'
logger.debug(msg.format(state, system_status_key, state_value))
state_value = 0
status = getattr(self, f'_compute_{system_status_key}_status')(state_value)
system_status.append(status)
return system_status
def _compute_component_status(self, state):
system_status = self._compute_system_status(state)
if const.ComponentStatusChoices.critical in system_status:
return const.ComponentStatusChoices.critical
elif const.ComponentStatusChoices.high in system_status:
return const.ComponentStatusChoices.high
else:
return const.ComponentStatusChoices.normal
@staticmethod
def _compute_component_status_display(status):
return getattr(const.ComponentStatusChoices, status).label
class TerminalStateMixin(ComputeStatusMixin):
CACHE_KEY_COMPONENT_STATE = 'CACHE_KEY_COMPONENT_STATE_TERMINAL_{}'
CACHE_TIMEOUT = 120
@property @property
def cache_key(self): def latest_status(self):
return self.CACHE_KEY_COMPONENT_STATE.format(str(self.id)) return Status.get_terminal_latest_status(self)
# get
def _get_from_cache(self):
return cache.get(self.cache_key)
def _set_to_cache(self, state):
cache.set(self.cache_key, state, self.CACHE_TIMEOUT)
# set
def _add_status(self, state):
status = self._compute_component_status(state)
status_display = self._compute_component_status_display(status)
state.update({
'status': status,
'status_display': status_display
})
@property @property
def state(self): def latest_status_display(self):
state = self._get_from_cache() return self.latest_status.label
return state or {}
@state.setter
def state(self, state):
self._add_status(state)
self._set_to_cache(state)
class TerminalStatusMixin(TerminalStateMixin):
# alive
@property
def is_alive(self):
return bool(self.state)
# status
@property
def status(self):
if self.is_alive:
return self.state['status']
else:
return const.ComponentStatusChoices.critical.value
@property @property
def status_display(self): def latest_stat(self):
return self._compute_component_status_display(self.status) return Status.get_terminal_latest_stat(self)
@property @property
def is_normal(self): def is_normal(self):
return self.status == const.ComponentStatusChoices.normal.value return self.latest_status == StatusChoice.normal
@property @property
def is_high(self): def is_high(self):
return self.status == const.ComponentStatusChoices.high.value return self.latest_status == StatusChoice.high
@property @property
def is_critical(self): def is_critical(self):
return self.status == const.ComponentStatusChoices.critical.value return self.latest_status == StatusChoice.critical
class Terminal(TerminalStatusMixin, models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name'))
type = models.CharField(
choices=const.TerminalTypeChoices.choices, default=const.TerminalTypeChoices.koko.value,
max_length=64, verbose_name=_('type')
)
remote_addr = models.CharField(max_length=128, blank=True, verbose_name=_('Remote Address'))
ssh_port = models.IntegerField(verbose_name=_('SSH Port'), default=2222)
http_port = models.IntegerField(verbose_name=_('HTTP Port'), default=5000)
command_storage = models.CharField(max_length=128, verbose_name=_("Command storage"), default='default')
replay_storage = models.CharField(max_length=128, verbose_name=_("Replay storage"), default='default')
user = models.OneToOneField(User, related_name='terminal', verbose_name='Application User', null=True, on_delete=models.CASCADE)
is_accepted = models.BooleanField(default=False, verbose_name='Is Accepted')
is_deleted = models.BooleanField(default=False)
date_created = models.DateTimeField(auto_now_add=True)
comment = models.TextField(blank=True, verbose_name=_('Comment'))
@property @property
def is_active(self): def is_alive(self):
if self.user and self.user.is_active: key = self.ALIVE_KEY.format(self.id)
return True # return self.latest_status != StatusChoice.offline
return False return cache.get(key, False)
@is_active.setter def set_alive(self, ttl=120):
def is_active(self, active): key = self.ALIVE_KEY.format(self.id)
if self.user: cache.set(key, True, ttl)
self.user.is_active = active
self.user.save()
class StorageMixin:
command_storage: str
replay_storage: str
def get_command_storage(self): def get_command_storage(self):
from .storage import CommandStorage from .storage import CommandStorage
@ -198,6 +93,44 @@ class Terminal(TerminalStatusMixin, models.Model):
config = self.get_replay_storage_config() config = self.get_replay_storage_config()
return {"TERMINAL_REPLAY_STORAGE": config} return {"TERMINAL_REPLAY_STORAGE": config}
class Terminal(StorageMixin, TerminalStatusMixin, models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name'))
type = models.CharField(
choices=const.TerminalTypeChoices.choices, default=const.TerminalTypeChoices.koko.value,
max_length=64, verbose_name=_('type')
)
remote_addr = models.CharField(max_length=128, blank=True, verbose_name=_('Remote Address'))
ssh_port = models.IntegerField(verbose_name=_('SSH Port'), default=2222)
http_port = models.IntegerField(verbose_name=_('HTTP Port'), default=5000)
command_storage = models.CharField(max_length=128, verbose_name=_("Command storage"), default='default')
replay_storage = models.CharField(max_length=128, verbose_name=_("Replay storage"), default='default')
user = models.OneToOneField(User, related_name='terminal', verbose_name='Application User', null=True, on_delete=models.CASCADE)
is_accepted = models.BooleanField(default=False, verbose_name='Is Accepted')
is_deleted = models.BooleanField(default=False)
date_created = models.DateTimeField(auto_now_add=True)
comment = models.TextField(blank=True, verbose_name=_('Comment'))
@property
def is_active(self):
if self.user and self.user.is_active:
return True
return False
@is_active.setter
def is_active(self, active):
if self.user:
self.user.is_active = active
self.user.save()
def get_online_sessions(self):
return Session.objects.filter(terminal=self, is_finished=False)
def get_online_session_count(self):
return self.get_online_sessions().count()
@staticmethod @staticmethod
def get_login_title_setting(): def get_login_title_setting():
login_title = None login_title = None

View File

@ -4,4 +4,3 @@ from .terminal import *
from .session import * from .session import *
from .storage import * from .storage import *
from .command import * from .command import *
from .components import *

View File

@ -1,25 +0,0 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
class ComponentsStateSerializer(serializers.Serializer):
# system
system_cpu_load_1 = serializers.FloatField(
required=False, label=_("System cpu load (1 minutes)")
)
system_memory_used_percent = serializers.FloatField(
required=False, label=_('System memory used percent')
)
system_disk_used_percent = serializers.FloatField(
required=False, label=_('System disk used percent')
)
# sessions
session_active_count = serializers.IntegerField(
required=False, label=_("Session active count")
)
def save(self, **kwargs):
request = self.context['request']
terminal = request.user.terminal
terminal.state = self.validated_data

View File

@ -9,15 +9,34 @@ from common.utils import get_request_ip
from ..models import ( from ..models import (
Terminal, Status, Session, Task, CommandStorage, ReplayStorage Terminal, Status, Session, Task, CommandStorage, ReplayStorage
) )
from .components import ComponentsStateSerializer
class StatusSerializer(serializers.ModelSerializer):
sessions = serializers.ListSerializer(
child=serializers.CharField(max_length=35), write_only=True
)
class Meta:
fields = [
'id',
'cpu_load', 'memory_used', 'disk_used',
'session_online', 'sessions',
'terminal', 'date_created',
]
extra_kwargs = {
"cpu_load": {'default': 0},
"memory_used": {'default': 0},
"disk_used": {'default': 0},
}
model = Status
class TerminalSerializer(BulkModelSerializer): class TerminalSerializer(BulkModelSerializer):
session_online = serializers.SerializerMethodField() session_online = serializers.SerializerMethodField()
is_alive = serializers.BooleanField(read_only=True) is_alive = serializers.BooleanField(read_only=True)
status = serializers.CharField(read_only=True) status = serializers.CharField(read_only=True, source='latest_status')
status_display = serializers.CharField(read_only=True) status_display = serializers.CharField(read_only=True, source='latest_status_display')
state = ComponentsStateSerializer(read_only=True) stat = StatusSerializer(read_only=True, source='latest_stat')
class Meta: class Meta:
model = Terminal model = Terminal
@ -25,7 +44,7 @@ class TerminalSerializer(BulkModelSerializer):
'id', 'name', 'type', 'remote_addr', 'http_port', 'ssh_port', 'id', 'name', 'type', 'remote_addr', 'http_port', 'ssh_port',
'comment', 'is_accepted', "is_active", 'session_online', 'comment', 'is_accepted', "is_active", 'session_online',
'is_alive', 'date_created', 'command_storage', 'replay_storage', 'is_alive', 'date_created', 'command_storage', 'replay_storage',
'status', 'status_display', 'state' 'status', 'status_display', 'stat'
] ]
read_only_fields = ['type', 'date_created'] read_only_fields = ['type', 'date_created']
@ -59,12 +78,6 @@ class TerminalSerializer(BulkModelSerializer):
return Session.objects.filter(terminal=obj, is_finished=False).count() return Session.objects.filter(terminal=obj, is_finished=False).count()
class StatusSerializer(serializers.ModelSerializer):
class Meta:
fields = ['id', 'terminal']
model = Status
class TaskSerializer(BulkModelSerializer): class TaskSerializer(BulkModelSerializer):
class Meta: class Meta:
fields = '__all__' fields = '__all__'

View File

@ -29,7 +29,7 @@ logger = get_task_logger(__name__)
@after_app_ready_start @after_app_ready_start
@after_app_shutdown_clean_periodic @after_app_shutdown_clean_periodic
def delete_terminal_status_period(): def delete_terminal_status_period():
yesterday = timezone.now() - datetime.timedelta(days=1) yesterday = timezone.now() - datetime.timedelta(days=7)
Status.objects.filter(date_created__lt=yesterday).delete() Status.objects.filter(date_created__lt=yesterday).delete()

View File

@ -35,7 +35,6 @@ urlpatterns = [
path('command-storages/<uuid:pk>/test-connective/', api.CommandStorageTestConnectiveApi.as_view(), name='command-storage-test-connective'), path('command-storages/<uuid:pk>/test-connective/', api.CommandStorageTestConnectiveApi.as_view(), name='command-storage-test-connective'),
# components # components
path('components/metrics/', api.ComponentsMetricsAPIView.as_view(), name='components-metrics'), path('components/metrics/', api.ComponentsMetricsAPIView.as_view(), name='components-metrics'),
path('components/state/', api.ComponentsStateAPIView.as_view(), name='components-state'),
# v2: get session's replay # v2: get session's replay
# path('v2/sessions/<uuid:pk>/replay/', # path('v2/sessions/<uuid:pk>/replay/',
# api.SessionReplayV2ViewSet.as_view({'get': 'retrieve'}), # api.SessionReplayV2ViewSet.as_view({'get': 'retrieve'}),

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import os import os
from itertools import groupby
from django.conf import settings from django.conf import settings
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
@ -10,9 +11,7 @@ import jms_storage
from common.tasks import send_mail_async from common.tasks import send_mail_async
from common.utils import get_logger, reverse from common.utils import get_logger, reverse
from settings.models import Setting
from . import const from . import const
from .models import ReplayStorage, Session, Command from .models import ReplayStorage, Session, Command
logger = get_logger(__name__) logger = get_logger(__name__)
@ -141,23 +140,73 @@ def send_command_execution_alert_mail(command):
send_mail_async.delay(subject, message, recipient_list, html_message=message) send_mail_async.delay(subject, message, recipient_list, html_message=message)
class ComponentsMetricsUtil(object): class ComputeStatUtil:
# system status
@staticmethod @staticmethod
def get_components(tp=None): def _common_compute_system_status(value, thresholds):
if thresholds[0] <= value <= thresholds[1]:
return const.ComponentStatusChoices.normal.value
elif thresholds[1] < value <= thresholds[2]:
return const.ComponentStatusChoices.high.value
else:
return const.ComponentStatusChoices.critical.value
@classmethod
def _compute_system_stat_status(cls, stat):
system_stat_thresholds_mapper = {
'cpu_load': [0, 5, 20],
'memory_used': [0, 85, 95],
'disk_used': [0, 80, 99]
}
system_status = {}
for stat_key, thresholds in system_stat_thresholds_mapper.items():
stat_value = getattr(stat, stat_key)
if stat_value is None:
msg = 'stat: {}, stat_key: {}, stat_value: {}'
logger.debug(msg.format(stat, stat_key, stat_value))
stat_value = 0
status = cls._common_compute_system_status(stat_value, thresholds)
system_status[stat_key] = status
return system_status
@classmethod
def compute_component_status(cls, stat):
if not stat:
return const.ComponentStatusChoices.offline
system_status_values = cls._compute_system_stat_status(stat).values()
if const.ComponentStatusChoices.critical in system_status_values:
return const.ComponentStatusChoices.critical
elif const.ComponentStatusChoices.high in system_status_values:
return const.ComponentStatusChoices.high
else:
return const.ComponentStatusChoices.normal
class TypedComponentsStatusMetricsUtil(object):
def __init__(self):
self.components = []
self.grouped_components = []
self.get_components()
def get_components(self):
from .models import Terminal from .models import Terminal
components = Terminal.objects.filter(is_deleted=False).order_by('type') components = Terminal.objects.filter(is_deleted=False).order_by('type')
if tp: grouped_components = groupby(components, lambda c: c.type)
components = components.filter(type=tp) grouped_components = [(i[0], list(i[1])) for i in grouped_components]
return components self.grouped_components = grouped_components
self.components = components
def get_metrics(self, tp=None): def get_metrics(self):
components = self.get_components(tp) metrics = []
total_count = normal_count = high_count = critical_count = offline_count = \ for _tp, components in self.grouped_components:
session_active_total = 0 normal_count = high_count = critical_count = 0
for component in components: total_count = offline_count = session_online_total = 0
total_count += 1
if component.is_alive: for component in components:
total_count += 1
if not component.is_alive:
offline_count += 1
continue
if component.is_normal: if component.is_normal:
normal_count += 1 normal_count += 1
elif component.is_high: elif component.is_high:
@ -165,20 +214,23 @@ class ComponentsMetricsUtil(object):
else: else:
# critical # critical
critical_count += 1 critical_count += 1
session_active_total += component.state.get('session_active_count', 0) session_online_total += component.get_online_session_count()
else: metrics.append({
offline_count += 1 'total': total_count,
return { 'normal': normal_count,
'total': total_count, 'high': high_count,
'normal': normal_count, 'critical': critical_count,
'high': high_count, 'offline': offline_count,
'critical': critical_count, 'session_active': session_online_total,
'offline': offline_count, 'type': _tp,
'session_active': session_active_total })
} return metrics
class ComponentsPrometheusMetricsUtil(ComponentsMetricsUtil): class ComponentsPrometheusMetricsUtil(TypedComponentsStatusMetricsUtil):
def __init__(self):
super().__init__()
self.metrics = self.get_metrics()
@staticmethod @staticmethod
def convert_status_metrics(metrics): def convert_status_metrics(metrics):
@ -190,50 +242,74 @@ class ComponentsPrometheusMetricsUtil(ComponentsMetricsUtil):
'offline': metrics['offline'] 'offline': metrics['offline']
} }
def get_prometheus_metrics_text(self): def get_component_status_metrics(self):
prometheus_metrics = list() prometheus_metrics = list()
# 各组件状态个数汇总 # 各组件状态个数汇总
prometheus_metrics.append('# JumpServer 各组件状态个数汇总') prometheus_metrics.append('# JumpServer 各组件状态个数汇总')
status_metric_text = 'jumpserver_components_status_total{component_type="%s", status="%s"} %s' status_metric_text = 'jumpserver_components_status_total{component_type="%s", status="%s"} %s'
for tp in const.TerminalTypeChoices.types(): for metric in self.metrics:
tp = metric['type']
prometheus_metrics.append(f'## 组件: {tp}') prometheus_metrics.append(f'## 组件: {tp}')
metrics_tp = self.get_metrics(tp) status_metrics = self.convert_status_metrics(metric)
status_metrics = self.convert_status_metrics(metrics_tp)
for status, value in status_metrics.items(): for status, value in status_metrics.items():
metric_text = status_metric_text % (tp, status, value) metric_text = status_metric_text % (tp, status, value)
prometheus_metrics.append(metric_text) prometheus_metrics.append(metric_text)
return prometheus_metrics
prometheus_metrics.append('\n') def get_component_session_metrics(self):
prometheus_metrics = list()
# 各组件在线会话数汇总 # 各组件在线会话数汇总
prometheus_metrics.append('# JumpServer 各组件在线会话数汇总') prometheus_metrics.append('# JumpServer 各组件在线会话数汇总')
session_active_metric_text = 'jumpserver_components_session_active_total{component_type="%s"} %s' session_active_metric_text = 'jumpserver_components_session_active_total{component_type="%s"} %s'
for tp in const.TerminalTypeChoices.types():
for metric in self.metrics:
tp = metric['type']
prometheus_metrics.append(f'## 组件: {tp}') prometheus_metrics.append(f'## 组件: {tp}')
metrics_tp = self.get_metrics(tp) metric_text = session_active_metric_text % (tp, metric['session_active'])
metric_text = session_active_metric_text % (tp, metrics_tp['session_active'])
prometheus_metrics.append(metric_text) prometheus_metrics.append(metric_text)
return prometheus_metrics
prometheus_metrics.append('\n') def get_component_stat_metrics(self):
prometheus_metrics = list()
# 各组件节点指标 # 各组件节点指标
prometheus_metrics.append('# JumpServer 各组件一些指标') prometheus_metrics.append('# JumpServer 各组件一些指标')
state_metric_text = 'jumpserver_components_%s{component_type="%s", component="%s"} %s' state_metric_text = 'jumpserver_components_%s{component_type="%s", component="%s"} %s'
states = [ stats_key = [
'cpu_load', 'memory_used', 'disk_used', 'session_online'
]
old_stats_key = [
'system_cpu_load_1', 'system_memory_used_percent', 'system_cpu_load_1', 'system_memory_used_percent',
'system_disk_used_percent', 'session_active_count' 'system_disk_used_percent', 'session_active_count'
] ]
for state in states: old_stats_key_mapper = dict(zip(stats_key, old_stats_key))
prometheus_metrics.append(f'## 指标: {state}')
components = self.get_components() for stat_key in stats_key:
for component in components: prometheus_metrics.append(f'## 指标: {stat_key}')
for component in self.components:
if not component.is_alive: if not component.is_alive:
continue continue
component_stat = component.latest_stat
if not component_stat:
continue
metric_text = state_metric_text % ( metric_text = state_metric_text % (
state, component.type, component.name, component.state.get(state) stat_key, component.type, component.name, getattr(component_stat, stat_key)
) )
prometheus_metrics.append(metric_text) prometheus_metrics.append(metric_text)
old_stat_key = old_stats_key_mapper.get(stat_key)
old_metric_text = state_metric_text % (
old_stat_key, component.type, component.name, getattr(component_stat, stat_key)
)
prometheus_metrics.append(old_metric_text)
return prometheus_metrics
def get_prometheus_metrics_text(self):
prometheus_metrics = list()
for method in [
self.get_component_status_metrics,
self.get_component_session_metrics,
self.get_component_stat_metrics
]:
prometheus_metrics.extend(method())
prometheus_metrics.append('\n')
prometheus_metrics_text = '\n'.join(prometheus_metrics) prometheus_metrics_text = '\n'.join(prometheus_metrics)
return prometheus_metrics_text return prometheus_metrics_text