!19 新功能(服务监控):添加服务监控功能;

Merge pull request !19 from xt12321/dvadmin-xt12321
pull/19/MERGE
李强 2021-04-25 15:20:46 +08:00 committed by Gitee
commit 665895f477
43 changed files with 1456 additions and 219 deletions

View File

@ -8,6 +8,8 @@ version: "3"
services: services:
dvadmin-ui: dvadmin-ui:
container_name: dvadmin-ui container_name: dvadmin-ui
ports:
- "8080:8080"
build: build:
context: ./ context: ./
dockerfile: ./docker_env/vue-ui/Dockerfile dockerfile: ./docker_env/vue-ui/Dockerfile

View File

@ -1 +0,0 @@

View File

@ -51,6 +51,7 @@ INSTALLED_APPS = [
'apps.vadmin.op_drf', 'apps.vadmin.op_drf',
'apps.vadmin.system', 'apps.vadmin.system',
'apps.vadmin.celery', 'apps.vadmin.celery',
'apps.vadmin.monitor',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -328,3 +329,5 @@ CELERYBEAT_SCHEDULER = 'django_celery_beat.schedulers.DatabaseScheduler' # Back
# ================================================= # # ================================================= #
# 接口权限 # 接口权限
INTERFACE_PERMISSION = locals().get("INTERFACE_PERMISSION", False) INTERFACE_PERMISSION = locals().get("INTERFACE_PERMISSION", False)
INTERFACE_PERMISSION = {locals().get("INTERFACE_PERMISSION", False)}
DJANGO_CELERY_BEAT_TZ_AWARE = False

View File

@ -22,7 +22,7 @@ from django.urls import re_path, include
from django.views.static import serve from django.views.static import serve
from rest_framework.views import APIView from rest_framework.views import APIView
from vadmin.utils.response import SuccessResponse from apps.vadmin.utils.response import SuccessResponse
class CaptchaRefresh(APIView): class CaptchaRefresh(APIView):

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class MonitorConfig(AppConfig):
name = 'vadmin.monitor'
verbose_name = "系统监控"

View File

@ -0,0 +1,23 @@
import django_filters
from .models import Server, Monitor
class ServerFilter(django_filters.rest_framework.FilterSet):
"""
服务器信息 简单过滤器
"""
class Meta:
model = Server
fields = '__all__'
class MonitorFilter(django_filters.rest_framework.FilterSet):
"""
服务器监控信息 简单过滤器
"""
class Meta:
model = Monitor
fields = '__all__'

View File

@ -0,0 +1,3 @@
from ..models.monitor import Monitor
from ..models.server import Server
from ..models.sys_files import SysFiles

View File

@ -0,0 +1,19 @@
from django.db.models import CharField, ForeignKey, CASCADE
from ...op_drf.models import CoreModel
class Monitor(CoreModel):
cpu_num = CharField(max_length=8, verbose_name='CPU核数')
cpu_sys = CharField(max_length=8, verbose_name='CPU已使用率')
mem_num = CharField(max_length=32, verbose_name='内存总数(KB)')
mem_sys = CharField(max_length=32, verbose_name='内存已使用大小(KB)')
seconds = CharField(max_length=32, verbose_name='系统已运行时间')
server = ForeignKey(to='monitor.Server', on_delete=CASCADE, verbose_name="关联服务器信息", db_constraint=False)
class Meta:
verbose_name = '服务器监控信息'
verbose_name_plural = verbose_name
def __str__(self):
return f"{self.server and self.server.name and self.server.ip}监控信息"

View File

@ -0,0 +1,20 @@
from django.db import models
from django.db.models import CharField
from apps.vadmin.op_drf.fields import UpdateDateTimeField, CreateDateTimeField
class Server(models.Model):
name = CharField(max_length=256, verbose_name='服务器名称', null=True, blank=True)
ip = CharField(max_length=32, verbose_name="ip地址")
os = CharField(max_length=32, verbose_name="操作系统")
remark = CharField(max_length=256, verbose_name="备注", null=True, blank=True)
update_datetime = UpdateDateTimeField() # 修改时间
create_datetime = CreateDateTimeField() # 创建时间
class Meta:
verbose_name = '服务器信息'
verbose_name_plural = verbose_name
def __str__(self):
return f"{self.name and self.ip}"

View File

@ -0,0 +1,19 @@
from django.db.models import CharField, ForeignKey, CASCADE
from ...op_drf.models import CoreModel
class SysFiles(CoreModel):
dir_name = CharField(max_length=32, verbose_name='磁盘路径')
sys_type_name = CharField(max_length=400, verbose_name='系统文件类型')
type_name = CharField(max_length=32, verbose_name='盘符类型')
total = CharField(max_length=32, verbose_name='磁盘总大小(KB)')
disk_sys = CharField(max_length=32, verbose_name='已使用大小(KB)')
monitor = ForeignKey(to='monitor.Monitor', on_delete=CASCADE, verbose_name="关联服务器监控信息", db_constraint=False)
class Meta:
verbose_name = '系统磁盘'
verbose_name_plural = verbose_name
def __str__(self):
return f"{self.creator and self.creator.name}"

View File

@ -0,0 +1,40 @@
from .models import Server, Monitor
from ..op_drf.serializers import CustomModelSerializer
# ================================================= #
# ************** 服务器信息 序列化器 ************** #
# ================================================= #
class ServerSerializer(CustomModelSerializer):
"""
服务器信息 简单序列化器
"""
class Meta:
model = Server
fields = ("id", "ip", "name", "os", "remark")
class UpdateServerSerializer(CustomModelSerializer):
"""
服务器信息 简单序列化器
"""
class Meta:
model = Server
fields = ("name", "remark")
# ================================================= #
# ************** 服务器监控信息 序列化器 ************** #
# ================================================= #
class MonitorSerializer(CustomModelSerializer):
"""
服务器监控信息 简单序列化器
"""
class Meta:
model = Monitor
fields = '__all__'

View File

@ -0,0 +1,89 @@
import datetime
import logging
import sys
import time
import psutil
from ..monitor.models import Server, Monitor, SysFiles
from ..op_drf.response import SuccessResponse
from ..system.models import ConfigSettings
from ..utils.decorators import BaseCeleryApp
logger = logging.getLogger(__name__)
from platform import platform
def getIP():
"""获取ipv4地址"""
dic = psutil.net_if_addrs()
ipv4_list = []
for adapter in dic:
snicList = dic[adapter]
for snic in snicList:
if snic.family.name == 'AF_INET':
ipv4 = snic.address
if ipv4 != '127.0.0.1':
ipv4_list.append(ipv4)
if len(ipv4_list) >= 1:
return ipv4_list[0]
else:
return None
@BaseCeleryApp(name='apps.vadmin.monitor.tasks.get_monitor_info')
def get_monitor_info():
"""
定时获取系统监控信息
:return:
"""
# 获取服务器
ip = getIP()
if not ip:
logger.error("无法获取到IP")
return
server_obj, create = Server.objects.get_or_create(ip=ip)
if create:
server_obj.name = ip
terse = ('terse' in sys.argv or '--terse' in sys.argv)
aliased = (not 'nonaliased' in sys.argv and not '--nonaliased' in sys.argv)
server_obj.os = platform(aliased, terse)
server_obj.save()
# 获取CPU和内存信息
mem = psutil.virtual_memory()
monitor_obj = Monitor()
monitor_obj.cpu_num = psutil.cpu_count()
monitor_obj.cpu_sys = float(psutil.cpu_percent(0.1))
monitor_obj.mem_num = int(mem.total / 1024)
monitor_obj.mem_sys = int(mem.used / 1024)
monitor_obj.seconds = time.strftime("%d%H 小时 %M 分 %S 秒", time.gmtime(int(time.time()) - int(psutil.boot_time())))
monitor_obj.server = server_obj
monitor_obj.save()
# 保存磁盘信息
for ele in psutil.disk_partitions():
disk = psutil.disk_usage('/')
sys_files_obj = SysFiles()
sys_files_obj.dir_name = ele.mountpoint
sys_files_obj.sys_type_name = ele.opts
sys_files_obj.type_name = ele.fstype
sys_files_obj.total = disk.total
sys_files_obj.disk_sys = disk.used
sys_files_obj.monitor = monitor_obj
sys_files_obj.save()
return SuccessResponse(msg="")
@BaseCeleryApp(name='apps.vadmin.monitor.tasks.clean_surplus_monitor_info')
def clean_surplus_monitor_info():
"""
定时清理多余 系统监控信息
:return:
"""
config_settings_obj = ConfigSettings.objects.filter(configKey='sys.monitor.info.save_days').first()
Monitor.objects.filter(
update_datetime__lt=datetime.timedelta(days=int(config_settings_obj.configValue or 30))).delete()
logger.info(f"成功清空{config_settings_obj.configValue}天前数据")

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,16 @@
from django.urls import re_path
from rest_framework.routers import DefaultRouter
from .views import ServerModelViewSet, MonitorModelViewSet
router = DefaultRouter()
router.register(r'server', ServerModelViewSet)
router.register(r'monitor', MonitorModelViewSet)
urlpatterns = [
re_path('monitor/info/(?P<pk>.*)/', MonitorModelViewSet.as_view({'get': 'get_monitor_info'})),
re_path('monitor/rate/(?P<pk>.*)/', MonitorModelViewSet.as_view({'get': 'get_rate_info'})),
re_path('monitor/enabled/', MonitorModelViewSet.as_view({'get': 'enabled_monitor_info'})),
re_path('monitor/clean/', MonitorModelViewSet.as_view({'get': 'clean_all'})),
]
urlpatterns += router.urls

View File

@ -0,0 +1,181 @@
from django_celery_beat.models import PeriodicTask, IntervalSchedule, CrontabSchedule
from rest_framework.request import Request
from .filters import ServerFilter, MonitorFilter
from .models import Server, Monitor, SysFiles
from .serializers import ServerSerializer, MonitorSerializer, UpdateServerSerializer
from ..op_drf.response import SuccessResponse, ErrorResponse
from ..op_drf.viewsets import CustomModelViewSet
from ..permission.permissions import CommonPermission
from ..system.models import ConfigSettings
class ServerModelViewSet(CustomModelViewSet):
"""
服务器信息 模型的CRUD视图
"""
queryset = Server.objects.all()
serializer_class = ServerSerializer
update_serializer_class = UpdateServerSerializer
# extra_filter_backends = [DataLevelPermissionsFilter]
filter_class = ServerFilter
update_extra_permission_classes = (CommonPermission,)
destroy_extra_permission_classes = (CommonPermission,)
create_extra_permission_classes = (CommonPermission,)
ordering = '-create_datetime' # 默认排序
class MonitorModelViewSet(CustomModelViewSet):
"""
服务器监控信息 模型的CRUD视图
"""
queryset = Monitor.objects.all()
serializer_class = MonitorSerializer
# extra_filter_backends = [DataLevelPermissionsFilter]
filter_class = MonitorFilter
update_extra_permission_classes = (CommonPermission,)
destroy_extra_permission_classes = (CommonPermission,)
create_extra_permission_classes = (CommonPermission,)
ordering = '-create_datetime' # 默认排序
def get_rate_info(self, request: Request, *args, **kwargs):
"""
获取使用率 监控信息
:param request:
:param args:
:param kwargs:
:return:
"""
pk = kwargs.get("pk")
queryset = self.filter_queryset(self.get_queryset())
queryset = queryset.filter(server__id=pk).order_by("-create_datetime")
# 间隔取值
queryset_count = queryset.count()
Interval_num = 1 if queryset_count < 200 else int(queryset_count / 100)
queryset = queryset.values('cpu_sys', 'mem_num', 'mem_sys')[::Interval_num][:100]
data = {
"cpu": [],
"memory": [],
}
for ele in queryset:
data["cpu"].append(float(ele.get('cpu_sys', 0)))
data["memory"].append(float(ele.get('mem_num', 0)) and round(float(ele.get('mem_sys', 0)) /
float(ele.get('mem_num', 0)), 4))
return SuccessResponse(data=data)
def get_monitor_info(self, request: Request, *args, **kwargs):
"""
最新的服务器状态信息
:param request:
:param args:
:param kwargs:
:return:
"""
pk = kwargs.get("pk")
instance = Monitor.objects.filter(server__id=pk).order_by("-create_datetime").first()
if not instance:
return ErrorResponse(msg="未找到服务器信息id")
serializer = self.get_serializer(instance)
data = serializer.data
return SuccessResponse(data={
"cpu": {
"total": int(data.get('cpu_num'), 0),
"used": "", # cpu核心 可不传如指cpu当前主频该值可以传
"rate": float(data.get('cpu_sys', 0)) / 100,
"unit": "核心", # 默认单位 核心
},
"memory": {
"total": int(int(data.get('mem_num', 0)) / 1024),
"used": int(int(data.get('mem_sys', 0)) / 1024),
"rate": int(data.get('mem_num', 0)) and round(int(data.get('mem_sys', 0)) /
int(data.get('mem_num', 0)), 4),
"unit": "MB", # 默认单位 MB
},
"disk": [{
"dir_name": ele.get('dir_name'),
"total": int(int(ele.get('total', 0)) / 1024 / 1024 / 1024),
"used": int(int(ele.get('disk_sys', 0)) / 1024 / 1024 / 1024),
"rate": int(ele.get('total', 0)) and round(int(ele.get('disk_sys', 0)) / int(ele.get('total', 0)),
4),
"unit": "GB", # 默认单位 GB
} for ele in SysFiles.objects.filter(monitor=instance).values('dir_name', 'total', 'disk_sys')]
})
def enabled_monitor_info(self, request: Request, *args, **kwargs):
"""
更新和获取监控信息
:param request:
:param args:
:param kwargs:
:return:
"""
enabled = request.query_params.get('enabled', None)
save_days = request.query_params.get('save_days', None)
interval = request.query_params.get('interval', None)
# 定时获取系统监控信息
periodictask_obj = PeriodicTask.objects.filter(task='apps.vadmin.monitor.tasks.get_monitor_info').first()
if not periodictask_obj:
intervalschedule_obj, _ = IntervalSchedule.objects.get_or_create(every=5, period="seconds")
periodictask_obj = PeriodicTask()
periodictask_obj.task = "apps.vadmin.monitor.tasks.get_monitor_info"
periodictask_obj.name = "定时获取系统监控信息"
periodictask_obj.interval = intervalschedule_obj
periodictask_obj.enabled = False
periodictask_obj.save()
# 定时清理多余 系统监控信息
clean_task_obj = PeriodicTask.objects.filter(
task='apps.vadmin.monitor.tasks.clean_surplus_monitor_info').first()
if not clean_task_obj:
crontab_obj, _ = CrontabSchedule.objects.get_or_create(day_of_month="*", day_of_week="*", hour="1",
minute="0", month_of_year="*")
clean_task_obj = PeriodicTask()
clean_task_obj.task = "apps.vadmin.monitor.tasks.clean_surplus_monitor_info"
clean_task_obj.name = "定时清理多余-系统监控信息"
clean_task_obj.crontab = crontab_obj
clean_task_obj.enabled = True
clean_task_obj.save()
# 配置添加
config_obj = ConfigSettings.objects.filter(configKey='sys.monitor.info.save_days').first()
if not config_obj:
config_obj = ConfigSettings()
config_obj.configKey = "sys.monitor.info.save_days"
config_obj.configName = "定时清理多余系统监控信息"
config_obj.configValue = "30"
config_obj.configType = "Y"
config_obj.status = "1"
config_obj.remark = "定时清理多余-系统监控信息默认30天"
config_obj.save()
if enabled:
# 更新监控状态
periodictask_obj.enabled = True if enabled == "1" else False
periodictask_obj.save()
# 更新 定时清理多余 系统监控信息 状态
clean_task_obj.enabled = True if enabled == "1" else False
clean_task_obj.save()
# 更新保留天数
if save_days and config_obj:
config_obj.configValue = save_days
config_obj.save()
# 更新监控获取频率
if interval:
periodictask_obj.interval.every = interval
periodictask_obj.interval.save()
return SuccessResponse(data={
"enabled": periodictask_obj.enabled,
"interval": periodictask_obj.interval.every,
"save_days": config_obj.configValue if config_obj else "30",
})
def clean_all(self, request: Request, *args, **kwargs):
"""
清空监控信息
:param request:
:param args:
:param kwargs:
:return:
"""
self.get_queryset().delete()
return SuccessResponse(msg="清空成功")

View File

@ -1,3 +1,4 @@
from django.conf import settings
from django.db import models from django.db import models
from django.db.models import SET_NULL from django.db.models import SET_NULL
@ -24,7 +25,7 @@ class CoreModel(models.Model):
增加审计字段, 覆盖字段时, 字段名称请勿修改, 必须统一审计字段名称 增加审计字段, 覆盖字段时, 字段名称请勿修改, 必须统一审计字段名称
""" """
description = DescriptionField() # 描述 description = DescriptionField() # 描述
creator = models.ForeignKey(to='permission.UserProfile', related_query_name='creator_query', null=True, creator = models.ForeignKey(to=settings.AUTH_USER_MODEL, related_query_name='creator_query', null=True,
verbose_name='创建者', on_delete=SET_NULL, db_constraint=False) # 创建者 verbose_name='创建者', on_delete=SET_NULL, db_constraint=False) # 创建者
modifier = ModifierCharField() # 修改者 modifier = ModifierCharField() # 修改者
dept_belong_id = models.CharField(max_length=64, verbose_name="数据归属部门", null=True, blank=True) dept_belong_id = models.CharField(max_length=64, verbose_name="数据归属部门", null=True, blank=True)

View File

@ -1,8 +1,11 @@
import django_filters import django_filters
from django.contrib.auth import get_user_model
from ..permission.models import Menu, Dept, Post, Role, UserProfile from ..permission.models import Menu, Dept, Post, Role
from ..utils.model_util import get_dept from ..utils.model_util import get_dept
UserProfile = get_user_model()
class MenuFilter(django_filters.rest_framework.FilterSet): class MenuFilter(django_filters.rest_framework.FilterSet):
""" """

View File

@ -1,6 +1,7 @@
import logging import logging
import os import os
from django.conf import settings
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db import connection from django.db import connection
@ -75,6 +76,7 @@ class Command(BaseCommand):
parser.add_argument('-N', nargs='*') parser.add_argument('-N', nargs='*')
def handle(self, *args, **options): def handle(self, *args, **options):
user_name = "_".join(settings.AUTH_USER_MODEL.lower().split("."))
init_dict = { init_dict = {
'system_dictdata': [os.path.join('system', 'system_dictdata.sql'), '字典管理', 'system_dictdata'], 'system_dictdata': [os.path.join('system', 'system_dictdata.sql'), '字典管理', 'system_dictdata'],
'system_dictdetails': [os.path.join('system', 'system_dictdetails.sql'), '字典详情', 'system_dictdetails'], 'system_dictdetails': [os.path.join('system', 'system_dictdetails.sql'), '字典详情', 'system_dictdetails'],
@ -86,8 +88,7 @@ class Command(BaseCommand):
'permission_role': [os.path.join('permission', 'permission_role.sql'), '角色管理', 'permission_role': [os.path.join('permission', 'permission_role.sql'), '角色管理',
','.join(['permission_role', 'permission_role_dept', 'permission_role_menu'])], ','.join(['permission_role', 'permission_role_dept', 'permission_role_menu'])],
'permission_userprofile': [os.path.join('permission', 'permission_userprofile.sql'), '用户管理', ','.join( 'permission_userprofile': [os.path.join('permission', 'permission_userprofile.sql'), '用户管理', ','.join(
['permission_userprofile_groups', 'permission_userprofile', 'permission_userprofile_role', [f'{user_name}_groups', f'{user_name}', f'{user_name}_role', f'{user_name}_post'])]
'permission_userprofile_post'])]
} }
init_name = options.get('init_name') init_name = options.get('init_name')
is_yes = None is_yes = None

View File

@ -11,7 +11,7 @@ class Dept(CoreModel):
phone = CharField(max_length=32, verbose_name="联系电话", null=True, blank=True) phone = CharField(max_length=32, verbose_name="联系电话", null=True, blank=True)
email = CharField(max_length=32, verbose_name="邮箱", null=True, blank=True) email = CharField(max_length=32, verbose_name="邮箱", null=True, blank=True)
status = CharField(max_length=8, verbose_name="部门状态", null=True, blank=True) status = CharField(max_length=8, verbose_name="部门状态", null=True, blank=True)
parentId = ForeignKey(to='Dept', on_delete=CASCADE, default=False, verbose_name="上级部门", parentId = ForeignKey(to='permission.Dept', on_delete=CASCADE, default=False, verbose_name="上级部门",
db_constraint=False, null=True, blank=True) db_constraint=False, null=True, blank=True)
class Meta: class Meta:

View File

@ -18,8 +18,8 @@ class Role(CoreModel):
admin = BooleanField(default=False, verbose_name="是否为admin") admin = BooleanField(default=False, verbose_name="是否为admin")
dataScope = CharField(max_length=8,default='1', choices=DATASCOPE_CHOICES, verbose_name="权限范围",) dataScope = CharField(max_length=8,default='1', choices=DATASCOPE_CHOICES, verbose_name="权限范围",)
remark = TextField(verbose_name="备注", help_text="备注", null=True, blank=True) remark = TextField(verbose_name="备注", help_text="备注", null=True, blank=True)
dept = ManyToManyField(to='Dept', verbose_name='数据权限-关联部门', db_constraint=False) dept = ManyToManyField(to='permission.Dept', verbose_name='数据权限-关联部门', db_constraint=False)
menu = ManyToManyField(to='Menu', verbose_name='关联菜单权限', db_constraint=False) menu = ManyToManyField(to='permission.Menu', verbose_name='关联菜单权限', db_constraint=False)
class Meta: class Meta:
verbose_name = '角色管理' verbose_name = '角色管理'

View File

@ -1,5 +1,6 @@
from uuid import uuid4 from uuid import uuid4
from django.conf import settings
from django.contrib.auth.models import UserManager, AbstractUser from django.contrib.auth.models import UserManager, AbstractUser
from django.core.cache import cache from django.core.cache import cache
from django.db.models import IntegerField, ForeignKey, CharField, TextField, ManyToManyField, CASCADE from django.db.models import IntegerField, ForeignKey, CharField, TextField, ManyToManyField, CASCADE
@ -22,9 +23,9 @@ class UserProfile(AbstractUser, CoreModel):
gender = CharField(max_length=8, verbose_name="性别", null=True, blank=True) gender = CharField(max_length=8, verbose_name="性别", null=True, blank=True)
remark = TextField(verbose_name="备注", null=True) remark = TextField(verbose_name="备注", null=True)
user_type = IntegerField(default=0, verbose_name="用户类型") user_type = IntegerField(default=0, verbose_name="用户类型")
post = ManyToManyField(to='Post', verbose_name='关联岗位', db_constraint=False) post = ManyToManyField(to='permission.Post', verbose_name='关联岗位', db_constraint=False)
role = ManyToManyField(to='Role', verbose_name='关联角色', db_constraint=False) role = ManyToManyField(to='permission.Role', verbose_name='关联角色', db_constraint=False)
dept = ForeignKey(to='Dept', verbose_name='归属部门', on_delete=CASCADE, db_constraint=False, null=True, blank=True) dept = ForeignKey(to='permission.Dept', verbose_name='归属部门', on_delete=CASCADE, db_constraint=False, null=True, blank=True)
@property @property
def get_user_interface_dict(self): def get_user_interface_dict(self):
@ -51,6 +52,7 @@ class UserProfile(AbstractUser, CoreModel):
return cache.delete(f'permission_interface_dict_{self.username}') return cache.delete(f'permission_interface_dict_{self.username}')
class Meta: class Meta:
abstract = settings.AUTH_USER_MODEL != 'permission.UserProfile'
verbose_name = '用户管理' verbose_name = '用户管理'
verbose_name_plural = verbose_name verbose_name_plural = verbose_name

View File

@ -1,10 +1,13 @@
from django.contrib.auth import get_user_model
from rest_framework import serializers from rest_framework import serializers
from rest_framework.validators import UniqueValidator from rest_framework.validators import UniqueValidator
from ..op_drf.serializers import CustomModelSerializer from ..op_drf.serializers import CustomModelSerializer
from ..permission.models import Menu, Dept, Post, Role, UserProfile from ..permission.models import Menu, Dept, Post, Role
from ..system.models import MessagePush from ..system.models import MessagePush
UserProfile = get_user_model()
# ================================================= # # ================================================= #
# ************** 菜单管理 序列化器 ************** # # ************** 菜单管理 序列化器 ************** #

View File

@ -1,4 +1,4 @@
from django.contrib.auth import authenticate from django.contrib.auth import authenticate, get_user_model
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.views import APIView from rest_framework.views import APIView
@ -6,7 +6,7 @@ from .permissions import CommonPermission, DeptDestroyPermission
from ..op_drf.filters import DataLevelPermissionsFilter from ..op_drf.filters import DataLevelPermissionsFilter
from ..op_drf.viewsets import CustomModelViewSet from ..op_drf.viewsets import CustomModelViewSet
from ..permission.filters import MenuFilter, DeptFilter, PostFilter, RoleFilter, UserProfileFilter from ..permission.filters import MenuFilter, DeptFilter, PostFilter, RoleFilter, UserProfileFilter
from ..permission.models import Role, Menu, Dept, Post, UserProfile from ..permission.models import Role, Menu, Dept, Post
from ..permission.serializers import UserProfileSerializer, MenuSerializer, RoleSerializer, \ from ..permission.serializers import UserProfileSerializer, MenuSerializer, RoleSerializer, \
MenuCreateUpdateSerializer, DeptSerializer, DeptCreateUpdateSerializer, PostSerializer, PostCreateUpdateSerializer, \ MenuCreateUpdateSerializer, DeptSerializer, DeptCreateUpdateSerializer, PostSerializer, PostCreateUpdateSerializer, \
RoleCreateUpdateSerializer, DeptTreeSerializer, MenuTreeSerializer, UserProfileCreateUpdateSerializer, \ RoleCreateUpdateSerializer, DeptTreeSerializer, MenuTreeSerializer, UserProfileCreateUpdateSerializer, \
@ -15,6 +15,8 @@ from ..permission.serializers import UserProfileSerializer, MenuSerializer, Role
from ..system.models import DictDetails from ..system.models import DictDetails
from ..utils.response import SuccessResponse, ErrorResponse from ..utils.response import SuccessResponse, ErrorResponse
UserProfile = get_user_model()
class GetUserProfileView(APIView): class GetUserProfileView(APIView):
""" """

View File

@ -1,5 +1,7 @@
import os import os
from django.conf import settings
def getSql(filename): def getSql(filename):
""" """
@ -9,6 +11,10 @@ def getSql(filename):
""" """
abspath = os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..")) abspath = os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(__file__)), ".."))
pwd = os.path.join(abspath, 'scripts', filename) pwd = os.path.join(abspath, 'scripts', filename)
with open(pwd,'rb') as fp: with open(pwd, 'rb') as fp:
content = fp.read().decode('utf8') content = fp.read().decode('utf8')
if filename == "permission/permission_userprofile.sql":
user_name = "_".join(settings.AUTH_USER_MODEL.lower().split("."))
content = content.replace("permission_userprofile", user_name). \
replace("userprofile", settings.AUTH_USER_MODEL.lower().split(".")[-1])
return [ele for ele in content.split('\n') if not ele.startswith('--') and ele.strip(' ')] return [ele for ele in content.split('\n') if not ele.startswith('--') and ele.strip(' ')]

View File

@ -9,7 +9,7 @@ class DictDetails(CoreModel):
is_default = BooleanField(verbose_name="是否默认", default=False) is_default = BooleanField(verbose_name="是否默认", default=False)
status = CharField(max_length=2, verbose_name="字典状态") status = CharField(max_length=2, verbose_name="字典状态")
sort = CharField(max_length=256, verbose_name="字典排序") sort = CharField(max_length=256, verbose_name="字典排序")
dict_data = ForeignKey(to='DictData', on_delete=CASCADE, verbose_name="关联字典", db_constraint=False) dict_data = ForeignKey(to='system.DictData', on_delete=CASCADE, verbose_name="关联字典", db_constraint=False)
remark = CharField(max_length=256, verbose_name="备注", null=True, blank=True) remark = CharField(max_length=256, verbose_name="备注", null=True, blank=True)
@classmethod @classmethod

View File

@ -1,9 +1,9 @@
from django.conf import settings
from django.db import models from django.db import models
from django.db.models import * from django.db.models import *
from ...op_drf.fields import UpdateDateTimeField, CreateDateTimeField from ...op_drf.fields import UpdateDateTimeField, CreateDateTimeField
from ...op_drf.models import CoreModel from ...op_drf.models import CoreModel
from ...permission.models import UserProfile
""" """
消息通知模型 消息通知模型
@ -17,7 +17,7 @@ class MessagePush(CoreModel):
is_reviewed = BooleanField(default=True, verbose_name="是否审核") is_reviewed = BooleanField(default=True, verbose_name="是否审核")
status = CharField(max_length=8, verbose_name="通知状态") status = CharField(max_length=8, verbose_name="通知状态")
to_path = CharField(max_length=256, verbose_name="跳转路径", null=True, blank=True, ) to_path = CharField(max_length=256, verbose_name="跳转路径", null=True, blank=True, )
user = ManyToManyField(to="permission.UserProfile", user = ManyToManyField(to=settings.AUTH_USER_MODEL,
related_name="user", related_query_name="user_query", through='MessagePushUser', related_name="user", related_query_name="user_query", through='MessagePushUser',
through_fields=('message_push', 'user')) through_fields=('message_push', 'user'))
@ -34,7 +34,7 @@ class MessagePushUser(models.Model):
related_name="messagepushuser_message_push", related_name="messagepushuser_message_push",
verbose_name='消息通知', help_text='消息通知') verbose_name='消息通知', help_text='消息通知')
user = ForeignKey(UserProfile, on_delete=CASCADE, db_constraint=False, user = ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=CASCADE, db_constraint=False,
related_name="messagepushuser_user", related_name="messagepushuser_user",
verbose_name='用户', help_text='用户') verbose_name='用户', help_text='用户')
is_read = BooleanField(default=False, verbose_name="是否已读") is_read = BooleanField(default=False, verbose_name="是否已读")

View File

@ -3,7 +3,7 @@ from rest_framework.routers import DefaultRouter
from ..system.views import DictDataModelViewSet, DictDetailsModelViewSet, \ from ..system.views import DictDataModelViewSet, DictDetailsModelViewSet, \
ConfigSettingsModelViewSet, SaveFileModelViewSet, MessagePushModelViewSet, LoginInforModelViewSet, \ ConfigSettingsModelViewSet, SaveFileModelViewSet, MessagePushModelViewSet, LoginInforModelViewSet, \
OperationLogModelViewSet, CeleryLogModelViewSet OperationLogModelViewSet, CeleryLogModelViewSet, SystemInfoApiView
router = DefaultRouter() router = DefaultRouter()
router.register(r'dict/type', DictDataModelViewSet) router.register(r'dict/type', DictDataModelViewSet)
@ -48,6 +48,7 @@ urlpatterns = [
re_path('celery_log/export/', CeleryLogModelViewSet.as_view({'get': 'export', })), re_path('celery_log/export/', CeleryLogModelViewSet.as_view({'get': 'export', })),
# 清除废弃文件 # 清除废弃文件
re_path('clearsavefile/', SaveFileModelViewSet.as_view({'post': 'clearsavefile', })), re_path('clearsavefile/', SaveFileModelViewSet.as_view({'post': 'clearsavefile', })),
# 获取系统信息cpu、内存、硬盘
re_path('sys/info/', SystemInfoApiView.as_view())
] ]
urlpatterns += router.urls urlpatterns += router.urls

View File

@ -4,6 +4,7 @@ from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Q from django.db.models import Q
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.views import APIView
from .models import LoginInfor, OperationLog, CeleryLog from .models import LoginInfor, OperationLog, CeleryLog
from ..op_drf.filters import DataLevelPermissionsFilter from ..op_drf.filters import DataLevelPermissionsFilter
@ -23,6 +24,7 @@ from ..system.serializers import DictDataSerializer, DictDataCreateUpdateSeriali
from ..utils.export_excel import export_excel_save_model from ..utils.export_excel import export_excel_save_model
from ..utils.file_util import get_all_files, remove_empty_dir, delete_files from ..utils.file_util import get_all_files, remove_empty_dir, delete_files
from ..utils.response import SuccessResponse from ..utils.response import SuccessResponse
from ..utils.system_info_utils import get_memory_used_percent, get_cpu_used_percent, get_disk_used_percent
class DictDataModelViewSet(CustomModelViewSet): class DictDataModelViewSet(CustomModelViewSet):
@ -329,3 +331,21 @@ class CeleryLogModelViewSet(CustomModelViewSet):
""" """
self.get_queryset().delete() self.get_queryset().delete()
return SuccessResponse(msg="清空成功") return SuccessResponse(msg="清空成功")
class SystemInfoApiView(APIView):
"""
系统服务监控视图
"""
def get(self, request, *args, **kwargs):
# 获取内存使用率
memory_used_percent = get_memory_used_percent()
# 获取cpu使用率
cpu_used_percent = get_cpu_used_percent()
# 获取硬盘使用率
disk_used_percent = get_disk_used_percent()
return SuccessResponse(data={"memory_used_percent": memory_used_percent,
"cpu_used_percent": cpu_used_percent,
"disk_used_percent": disk_used_percent
})

View File

@ -54,5 +54,6 @@ urlpatterns = [
re_path(r'^permission/', include('apps.vadmin.permission.urls')), re_path(r'^permission/', include('apps.vadmin.permission.urls')),
re_path(r'^system/', include('apps.vadmin.system.urls')), re_path(r'^system/', include('apps.vadmin.system.urls')),
re_path(r'^celery/', include('apps.vadmin.celery.urls')), re_path(r'^celery/', include('apps.vadmin.celery.urls')),
re_path(r'^monitor/', include('apps.vadmin.monitor.urls')),
] ]

View File

@ -4,6 +4,27 @@
import psutil as psutil import psutil as psutil
def get_cpu_info():
"""
获取cpu所有信息
"""
pass
def get_memory_info():
"""
获取内存所有信息
"""
pass
def get_disk_info():
"""
获取硬盘所有信息
"""
pass
def get_cpu_used_percent(): def get_cpu_used_percent():
""" """
获取CPU运行情况 获取CPU运行情况
@ -31,7 +52,5 @@ def get_disk_used_percent():
pass pass
if __name__ == '__main__': if __name__ == '__main__':
get_cpu_used_percent() get_disk_used_percent()

View File

@ -3,7 +3,7 @@ ENV = 'development'
# 若依管理系统/开发环境 # 若依管理系统/开发环境
# VUE_APP_BASE_API = 'https://api.django-vue-admin.com' # VUE_APP_BASE_API = 'https://api.django-vue-admin.com'
VUE_APP_BASE_API = 'http://127.0.0.1:8000' VUE_APP_BASE_API = 'http://127.0.0.1:8888'
# 路由懒加载 # 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true VUE_CLI_BABEL_TRANSPILE_MODULES = true

View File

@ -41,14 +41,16 @@
"axios": "0.21.0", "axios": "0.21.0",
"clipboard": "2.0.6", "clipboard": "2.0.6",
"core-js": "3.8.1", "core-js": "3.8.1",
"echarts": "4.9.0", "echarts": "^4.9.0",
"element-ui": "2.15.0", "element-ui": "2.15.0",
"eslint-loader": "^4.0.2",
"file-saver": "2.0.4", "file-saver": "2.0.4",
"fuse.js": "6.4.3", "fuse.js": "6.4.3",
"highlight.js": "9.18.5", "highlight.js": "9.18.5",
"js-beautify": "1.13.0", "js-beautify": "1.13.0",
"js-cookie": "2.2.1", "js-cookie": "2.2.1",
"jsencrypt": "3.0.0-rc.1", "jsencrypt": "3.0.0-rc.1",
"lodash": "^4.17.21",
"moment": "^2.29.1", "moment": "^2.29.1",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"quill": "1.3.7", "quill": "1.3.7",
@ -58,6 +60,7 @@
"vue-count-to": "1.0.13", "vue-count-to": "1.0.13",
"vue-cropper": "0.5.5", "vue-cropper": "0.5.5",
"vue-router": "3.4.9", "vue-router": "3.4.9",
"vue-types": "^2.0.3",
"vuedraggable": "2.24.3", "vuedraggable": "2.24.3",
"vuex": "3.6.0" "vuex": "3.6.0"
}, },
@ -65,6 +68,7 @@
"@vue/cli-plugin-babel": "4.4.6", "@vue/cli-plugin-babel": "4.4.6",
"@vue/cli-plugin-eslint": "4.4.6", "@vue/cli-plugin-eslint": "4.4.6",
"@vue/cli-service": "4.4.6", "@vue/cli-service": "4.4.6",
"@vue/composition-api": "^1.0.0-rc.6",
"babel-eslint": "10.1.0", "babel-eslint": "10.1.0",
"chalk": "4.1.0", "chalk": "4.1.0",
"connect": "3.6.6", "connect": "3.6.6",

View File

@ -3,7 +3,7 @@ import request from '@/utils/request'
// 获取路由 // 获取路由
export const getRouters = () => { export const getRouters = () => {
return request({ return request({
url: '/admin/getRouters', url: '/admin/getRouters/',
method: 'get' method: 'get'
}) })
} }

View File

@ -3,7 +3,7 @@ import request from '@/utils/request'
// 查询在线用户列表 // 查询在线用户列表
export function list(query) { export function list(query) {
return request({ return request({
url: '/monitor/online/list', url: '/admin/monitor/online/list',
method: 'get', method: 'get',
params: query params: query
}) })
@ -12,7 +12,7 @@ export function list(query) {
// 强退用户 // 强退用户
export function forceLogout(tokenId) { export function forceLogout(tokenId) {
return request({ return request({
url: '/monitor/online/' + tokenId, url: '/admin/monitor/online/' + tokenId,
method: 'delete' method: 'delete'
}) })
} }

View File

@ -1,9 +1,65 @@
import request from '@/utils/request' import request from '@/utils/request'
// 查询服务器详细 // 查询服务器信息详细
export function getServer() { export function getServerList(params) {
return request({ return request({
url: '/monitor/server', url: 'admin/monitor/server/',
params,
method: 'get' method: 'get'
}) })
} }
// 修改服务器信息
export function updateServerInfo(id, data) {
let {name, remark} = data;
return request({
url: `admin/monitor/server/${id}/`,
data: {
name,
remark
},
method: 'PUT'
})
}
// 获取监控配置信息
export function getMonitorStatusInfo() {
return request({
url: 'admin/monitor/monitor/enabled/',
method: 'get'
})
}
// 更新监控配置信息
export function updateMonitorStatusInfo(params) {
return request({
url: 'admin/monitor/monitor/enabled/',
params,
method: 'get'
})
}
// 清空记录
export function cleanMonitorLog() {
return request({
url: 'admin/monitor/monitor/clean/',
method: 'delete'
})
}
// 获取监控记录
export function getMonitorLogs(id, params) {
return request({
url: `admin/monitor/monitor/rate/${id}/`,
params,
method: 'get'
})
}
// 获取服务器最新监控日志信息
export function getServerLatestLog(id) {
return request({
url: `admin/monitor/monitor/info/${id}/`,
method: 'get'
})
}

View File

@ -24,11 +24,28 @@ const permission = {
return new Promise(resolve => { return new Promise(resolve => {
// 向后端请求路由数据 // 向后端请求路由数据
getRouters().then(res => { getRouters().then(res => {
const data = handleTree(res.data, "id"); let tempData = handleTree(res.data, "id");
tempData[2].children.push({
component: "vadmin/monitor/server/index",
hidden: false,
id: 97,
meta: {title: "服务监控", icon: "server", noCache: false},
name: "server",
orderNum: 3,
parentId: 66,
path: "server",
redirect: "server"
})
const data = tempData
// console.log("handleTree:", data)
const sdata = JSON.parse(JSON.stringify(data)) const sdata = JSON.parse(JSON.stringify(data))
const rdata = JSON.parse(JSON.stringify(data)) const rdata = JSON.parse(JSON.stringify(data))
const sidebarRoutes = filterAsyncRouter(sdata) const sidebarRoutes = filterAsyncRouter(sdata)
// console.log(sidebarRoutes)
const rewriteRoutes = filterAsyncRouter(rdata, false, true) const rewriteRoutes = filterAsyncRouter(rdata, false, true)
// console.log(rewriteRoutes)
rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true }) rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true })
commit('SET_ROUTES', rewriteRoutes) commit('SET_ROUTES', rewriteRoutes)
commit('SET_SIDEBAR_ROUTERS', sidebarRoutes) commit('SET_SIDEBAR_ROUTERS', sidebarRoutes)

View File

@ -0,0 +1,215 @@
<template>
<div class="instrument-board">
<div v-if="showTopTitle && haveMultipleData" class="instrument-board-title">
<el-select :value="topTitle"
@change="chooseDisplayInstrumentBoardData"
>
<el-option
v-for="(item,index) in instrumentBoardData"
:key="index"
:label="item.name || item['dir_name']"
:value="index"
>
</el-option>
</el-select>
</div>
<div v-else-if="showTopTitle" class="instrument-board-title">
{{ topTitle }}
</div>
<div :id="ringGraphId" class="instrument-board-body"></div>
<div v-if="showSubTitle"
class="instrument-board-subtitle"
:title="subTitle.title"
>{{ subTitle.content }}
</div>
</div>
</template>
<script>
import VueTypes from 'vue-types'
// ,
const echarts = require('echarts/lib/echarts')
require('echarts/lib/chart/gauge')
//
const NORMAL_COLOR = {
color: '#28BCFE',
itemColor: ['#25bfff', '#5284de', '#2a95f9']
}
const WARNING_COLOR = {
color: '#e6a23c',
itemColor: ['#e6a23c', '#cc8b1d', '#ffaf18']
}
const DANGER_COLOR = {
color: '#F56C6C',
itemColor: ['#fd666d', '#cf1717', '#b31212']
}
export default {
name: 'InstrumentBoard',
props: {
// key
ringGraphKey: VueTypes.string.isRequired,
//
showTopTitle: VueTypes.bool.def(false),
//
showSubTitle: VueTypes.bool.def(false),
// top title
topTitleKeyToNameMapping: VueTypes.object.def({
cpu: 'CPU使用率',
memory: '内存使用率'
}),
instrumentBoardData: VueTypes.any.isRequired
},
data() {
return {
//
currentInstrumentBoardData: {}
}
},
computed: {
//
haveMultipleData() {
return this.instrumentBoardData instanceof Array && this.instrumentBoardData.length > 0
},
// 使
ringRate() {
let ringRate = this.currentInstrumentBoardData.rate
return ringRate < 1 ? ringRate * 100 : ringRate
},
// id
ringGraphId() {
return `${this.ringGraphKey}UsingRate`
},
//
topTitle() {
return this.currentInstrumentBoardData['dir_name'] || this.topTitleKeyToNameMapping[this.ringGraphKey] || this.ringGraphKey
},
//
subTitle() {
let used = this.currentInstrumentBoardData['used'] ? this.currentInstrumentBoardData['used'] + '/' : ''
let total = this.currentInstrumentBoardData['total'] ? this.currentInstrumentBoardData['total'] : ''
let unit = this.currentInstrumentBoardData['unit'] ? ` (${this.currentInstrumentBoardData['unit']})` : ''
let content = `${used}${total}${unit} `
let title = (this.currentInstrumentBoardData['used'] ? '已用/' : '') + '总量(单位)'
return { content, title }
},
// 使
usingRateStyle() {
return {
fontSize: 18,
...this.getCircleColor(this.ringRate)
}
}
},
mounted() {
if (this.haveMultipleData) {
this.currentInstrumentBoardData = this.instrumentBoardData[0]
} else {
this.currentInstrumentBoardData = this.instrumentBoardData
}
this.drawBar()
},
methods: {
drawBar() {
let currentRate = [this.ringRate]
// domecharts
let RingGraph = echarts.init(document.getElementById(this.ringGraphId))
let option = {
title: {
text: currentRate + '%',
textStyle: this.usingRateStyle,
itemGap: 10,
left: 'center',
top: '45%'
},
angleAxis: {
max: 100,
clockwise: true, //
// 线
show: false
},
radiusAxis: {
type: 'category',
show: true,
axisLabel: {
show: false
},
axisLine: {
show: false
},
axisTick: {
show: false
}
},
polar: {
center: ['50%', '50%'], //
radius: '100%' //
},
series: [{
type: 'bar',
data: currentRate,
showBackground: true,
backgroundStyle: {
color: '#BDEBFF' //
},
coordinateSystem: 'polar',
roundCap: true,
barWidth: 15,
itemStyle: {
normal: {
opacity: 1,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: this.usingRateStyle.itemColor[0] || '#25BFFF'
}, {
offset: 1,
color: this.usingRateStyle.itemColor[1] || '#5284DE'
}]),
shadowBlur: 1,
shadowColor: this.usingRateStyle.itemColor[2] || '#2A95F9'
}
}
}]
}
//
RingGraph.setOption(option)
},
// -
getCircleColor(usingRate) {
if (usingRate < 60) {
return NORMAL_COLOR
} else if (usingRate > 60 && usingRate < 80) {
return WARNING_COLOR
} else if (usingRate > 80) {
return DANGER_COLOR
}
return NORMAL_COLOR
},
chooseDisplayInstrumentBoardData(index) {
this.currentInstrumentBoardData = this.instrumentBoardData[index]
this.drawBar()
}
}
}
</script>
<style scoped>
.instrument-board-title {
font-weight: bolder;
text-align: center;
}
.instrument-board-body {
min-height: 200px;
min-width: 200px;
}
.instrument-board-subtitle {
text-align: center;
}
</style>

View File

@ -0,0 +1,181 @@
<template>
<div>
<div class="line-chart-title">
<div class="line-chart-name">{{ chartTitle }}</div>
<div class="line-chart-time-radio">
<el-radio-group v-model="timeLimit" @change="changeTimeLimit">
<el-radio-button
v-for="item in Object.keys(TIME_LIMIT_SETTING)"
:label="TIME_LIMIT_SETTING[item].name"
:key="TIME_LIMIT_SETTING[item].key"
></el-radio-button>
</el-radio-group>
</div>
</div>
<div :id="lineChartId" class="line-chart-body"></div>
</div>
</template>
<script>
import VueTypes from 'vue-types'
// ,
import echarts from 'echarts'
import moment from 'moment'
import { getMonitorLogs } from '@/api/vadmin/monitor/server'
const MONTH = moment().month()
const YEAR = moment().year()
const TODAY = moment().format('YYYY-MM-DD')
const YESTERDAY = moment().subtract(1, 'days').format('YYYY-MM-DD')
const LAST_SEVEN_DAYS = moment().subtract(7, 'days').format('YYYY-MM-DD')
const LAST_THIRTY_DAYS = moment().subtract(30, 'days').format('YYYY-MM-DD')
//
const TIME_LIMIT_SETTING = {
'yesterday': {
key: 'yesterday',
name: '昨天',
timeRange: [
`${YESTERDAY} 00:00:00`,
`${YESTERDAY} 23:59:59`
]
},
'today': {
key: 'today',
name: '今天',
timeRange: [
`${TODAY} 00:00:00`,
`${TODAY} 23:59:59`
]
},
'latestWeek': {
key: 'latestWeek',
name: '最近7天',
timeRange: [
`${LAST_SEVEN_DAYS} 00:00:00`,
`${TODAY} 23:59:59`
]
},
'latestMonth': {
key: 'latestMonth',
name: '最近30天',
timeRange: [
`${LAST_THIRTY_DAYS} 00:00:00`,
`${TODAY} 23:59:59`
]
}
}
//
const DEFAULT_TIME = '今天'
export default {
name: 'LineChart',
props: {
serverInfo: VueTypes.object.isRequired,
lineChartKey: VueTypes.string.isRequired,
chartTitle: VueTypes.string.isRequired,
chartData: VueTypes.array.isRequired
},
data() {
return {
TIME_LIMIT_SETTING,
timeLimit: DEFAULT_TIME,
lineChartId: this.lineChartKey + 'Chart',
lineChartData: this.chartData
}
},
mounted() {
this.drawBar()
},
computed: {
timeLimitNames() {
return Object.keys(TIME_LIMIT_SETTING).map(item => {
return TIME_LIMIT_SETTING[item].name
})
},
currentDateIndex() {
return this.timeLimitNames.indexOf(this.timeLimit)
},
currentTimeLimitSetting() {
return TIME_LIMIT_SETTING[Object.keys(TIME_LIMIT_SETTING)[this.currentDateIndex]]
}
},
methods: {
drawBar() {
// domecharts
let barGraph = echarts.init(document.getElementById(this.lineChartId))
//
barGraph.setOption({
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c}'
},
legend: {
left: 'center',
data: [],
bottom: 0
},
xAxis: {
type: 'category',
name: 'x',
splitLine: { show: false },
data: []
},
grid: {
left: '1%',
right: '2%',
bottom: '8%',
containLabel: true
},
yAxis: {
type: 'value',
name: '使用率',
},
series: [
{
name: '使用率',
type: 'line',
data: this.lineChartData
}
]
})
},
changeTimeLimit(value) {
this.timeLimit = value
this.getCurrentServerMonitorLogs()
},
getCurrentServerMonitorLogs() {
getMonitorLogs(this.serverInfo.id,{ as: { 'create_datetime__range': this.currentTimeLimitSetting.timeRange } }).then(results => {
this.lineChartData = results.data[this.lineChartKey]
this.drawBar()
}).catch(error => {
this.$message.warning(error.msg || `获取${this.chartTitle}数据失败!`)
})
}
}
}
</script>
<style scoped>
.line-chart-name {
font-weight: bolder;
width: 20%;
min-width: 30px;
text-align: left;
display: inline-block;
}
.line-chart-time-radio {
font-weight: bolder;
width: 80%;
min-width: 200px;
text-align: right;
display: inline-block;
}
.line-chart-body {
min-height: 300px;
min-width: 300px;
}
</style>

View File

@ -1,210 +1,493 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<el-row> <!-- 监控控制 -->
<el-col :span="12" class="card-box"> <div class="server-monitor-control">
<el-card> <!-- 监控启用开关 -->
<div slot="header"><span>CPU</span></div> <div class="control-server-monitor same-block">
<div class="el-table el-table--enable-row-hover el-table--medium"> 开启监控
<table cellspacing="0" style="width: 100%;"> <el-switch
<thead> v-model="isOpeningMonitor"
<tr> active-color="#13ce66"
<th class="is-leaf"><div class="cell">属性</div></th> inactive-color="#ff4949"
<th class="is-leaf"><div class="cell"></div></th> title="控制所有监控项"
</tr> @change="changeMonitorStatus"
</thead> >
<tbody> </el-switch>
<tr> </div>
<td><div class="cell">核心数</div></td> <!-- 更新频率设置 -->
<td><div class="cell" v-if="server.cpu">{{ server.cpu.cpuNum }}</div></td> <div class="monitor-update-interval same-block">
</tr> 监控频率
<tr> <el-input-number v-model="monitorUpdateInterval"
<td><div class="cell">用户使用率</div></td> label=""
<td><div class="cell" v-if="server.cpu">{{ server.cpu.used }}%</div></td> class="monitor-update-interval-blank"
</tr> controls-position="right"
<tr> :min="minMonitorUpdateInterval"
<td><div class="cell">系统使用率</div></td> @input="handleIntervalChange"
<td><div class="cell" v-if="server.cpu">{{ server.cpu.sys }}%</div></td>
</tr>
<tr>
<td><div class="cell">当前空闲率</div></td>
<td><div class="cell" v-if="server.cpu">{{ server.cpu.free }}%</div></td>
</tr>
</tbody>
</table>
</div>
</el-card>
</el-col>
<el-col :span="12" class="card-box"> ></el-input-number>
<el-card> <el-select v-model="intervalType"
<div slot="header"><span>内存</span></div> class="monitor-update-interval-unit"
<div class="el-table el-table--enable-row-hover el-table--medium"> @change="selectIntervalType"
<table cellspacing="0" style="width: 100%;"> >
<thead> <el-option
<tr> v-for="item in Object.keys(INTERVAL_ID_TO_TYPE_MAPPING)"
<th class="is-leaf"><div class="cell">属性</div></th> :key="INTERVAL_ID_TO_TYPE_MAPPING[item].type"
<th class="is-leaf"><div class="cell">内存</div></th> :label="INTERVAL_ID_TO_TYPE_MAPPING[item].name"
<th class="is-leaf"><div class="cell">JVM</div></th> :value="INTERVAL_ID_TO_TYPE_MAPPING[item].name">
</tr> </el-option>
</thead> </el-select>
<tbody>
<tr>
<td><div class="cell">总内存</div></td>
<td><div class="cell" v-if="server.mem">{{ server.mem.total }}G</div></td>
<td><div class="cell" v-if="server.jvm">{{ server.jvm.total }}M</div></td>
</tr>
<tr>
<td><div class="cell">已用内存</div></td>
<td><div class="cell" v-if="server.mem">{{ server.mem.used}}G</div></td>
<td><div class="cell" v-if="server.jvm">{{ server.jvm.used}}M</div></td>
</tr>
<tr>
<td><div class="cell">剩余内存</div></td>
<td><div class="cell" v-if="server.mem">{{ server.mem.free }}G</div></td>
<td><div class="cell" v-if="server.jvm">{{ server.jvm.free }}M</div></td>
</tr>
<tr>
<td><div class="cell">使用率</div></td>
<td><div class="cell" v-if="server.mem" :class="{'text-danger': server.mem.usage > 80}">{{ server.mem.usage }}%</div></td>
<td><div class="cell" v-if="server.jvm" :class="{'text-danger': server.jvm.usage > 80}">{{ server.jvm.usage }}%</div></td>
</tr>
</tbody>
</table>
</div>
</el-card>
</el-col>
<el-col :span="24" class="card-box"> </div>
<el-card> <!-- 监控日志保存时间 -->
<div slot="header"> <div class="monitor-log-save-time same-block">
<span>服务器信息</span> 保存天数
</div> <el-input v-model="monitorLogSavingDays" class=" same-block" style="width: 120px;"></el-input>
<div class="el-table el-table--enable-row-hover el-table--medium"> <el-button type="primary"
<table cellspacing="0" style="width: 100%;"> class="same-block"
<tbody> title="只有提交更改才会生效"
<tr> @click="updateMonitorStatusSettingsInfo"
<td><div class="cell">服务器名称</div></td> >更改
<td><div class="cell" v-if="server.sys">{{ server.sys.computerName }}</div></td> </el-button>
<td><div class="cell">操作系统</div></td> </div>
<td><div class="cell" v-if="server.sys">{{ server.sys.osName }}</div></td> <!-- 清空记录 -->
</tr> <div class="clean-monitor-log same-block">
<tr> <el-button class="same-block"
<td><div class="cell">服务器IP</div></td> type="warning"
<td><div class="cell" v-if="server.sys">{{ server.sys.computerIp }}</div></td> title="清空所有监控记录"
<td><div class="cell">系统架构</div></td> @click="cleanMonitorLogsInfo"
<td><div class="cell" v-if="server.sys">{{ server.sys.osArch }}</div></td> >清空记录
</tr> </el-button>
</tbody> </div>
</table> </div>
</div>
</el-card>
</el-col>
<el-col :span="24" class="card-box"> <div class="server-monitor-top">
<el-card> <!-- 左侧服务器信息 -->
<div slot="header"> <el-card class="box-card server-information">
<span>Java虚拟机信息</span> <div slot="header" class="clearfix">
<div class="server-info-item">服务器</div>
<el-select filterable
:value="currentServerName"
class="server-info-item"
placeholder="请选择服务器"
@change="chooseServerInfo"
>
<el-option
v-for="(item,index) in allServerInfo"
:key="item.id"
:label="item.name"
:value="index"
>
</el-option>
</el-select>
<el-button type="primary"
class="server-info-item"
title="只有提交更改才会生效"
@click="updateServerInfo"
>更改
</el-button>
</div>
<div class="server-info-detail">
<div v-for="(key,index) in currentServerInfoKeys" :key="index" class="server-info-detail-line text item">
<div class="server-info-detail-item">
<div style="width: 30%;display: inline-block;">{{ SERVER_KEY_TO_NAME_MAPPING[key] }}:</div>
<div v-if="CHANGEABLE_SERVER_FIELDS.indexOf(key) > -1" style="display: inline-block;">
<el-input style="display: inline-block; width: 90%;" v-model="currentServer[key]"></el-input>
</div>
<div v-else style="display: inline-block; "> {{ currentServer[key] }}</div>
</div>
</div> </div>
<div class="el-table el-table--enable-row-hover el-table--medium"> </div>
<table cellspacing="0" style="width: 100%;"> </el-card>
<tbody>
<tr>
<td><div class="cell">Java名称</div></td>
<td><div class="cell" v-if="server.jvm">{{ server.jvm.name }}</div></td>
<td><div class="cell">Java版本</div></td>
<td><div class="cell" v-if="server.jvm">{{ server.jvm.version }}</div></td>
</tr>
<tr>
<td><div class="cell">启动时间</div></td>
<td><div class="cell" v-if="server.jvm">{{ server.jvm.startTime }}</div></td>
<td><div class="cell">运行时长</div></td>
<td><div class="cell" v-if="server.jvm">{{ server.jvm.runTime }}</div></td>
</tr>
<tr>
<td colspan="1"><div class="cell">安装路径</div></td>
<td colspan="3"><div class="cell" v-if="server.jvm">{{ server.jvm.home }}</div></td>
</tr>
<tr>
<td colspan="1"><div class="cell">项目路径</div></td>
<td colspan="3"><div class="cell" v-if="server.sys">{{ server.sys.userDir }}</div></td>
</tr>
</tbody>
</table>
</div>
</el-card>
</el-col>
<el-col :span="24" class="card-box"> <!-- 右侧仪表盘 -->
<el-card> <el-card class="box-card information-instrument-panel"
<div slot="header"> v-for="(key, index) of Object.keys(instrumentBoardData)"
<span>磁盘状态</span> :key="`${index}-${key}`">
</div> <instrument-board
<div class="el-table el-table--enable-row-hover el-table--medium"> :show-top-title="true"
<table cellspacing="0" style="width: 100%;"> :show-sub-title="true"
<thead> :ring-graph-key="key"
<tr> :instrument-board-data="instrumentBoardData[key]"
<th class="is-leaf"><div class="cell">盘符路径</div></th> :top-title-key-to-name-mapping="INSTRUMENT_BOARD_KEY_TO_NAME_MAPPING"
<th class="is-leaf"><div class="cell">文件系统</div></th> ></instrument-board>
<th class="is-leaf"><div class="cell">盘符类型</div></th> </el-card>
<th class="is-leaf"><div class="cell">总大小</div></th> </div>
<th class="is-leaf"><div class="cell">可用大小</div></th> <!-- 下方折线图 -->
<th class="is-leaf"><div class="cell">已用大小</div></th> <div class="server-monitor-bottom">
<th class="is-leaf"><div class="cell">已用百分比</div></th> <!-- 折线图 -->
</tr> <el-card class="box-card server-monitor-line-chart" v-for="(key, index) in Object.keys(lineChartData)"
</thead> :key="`${index}-${key}`">
<tbody v-if="server.sysFiles"> <line-chart :line-chart-key="key"
<tr v-for="sysFile in server.sysFiles"> :server-info="currentServer"
<td><div class="cell">{{ sysFile.dirName }}</div></td> :chart-title="CHART_KEY_NAME_MAPPING[key]"
<td><div class="cell">{{ sysFile.sysTypeName }}</div></td> :chart-data="lineChartData[key]"
<td><div class="cell">{{ sysFile.typeName }}</div></td> ></line-chart>
<td><div class="cell">{{ sysFile.total }}</div></td> </el-card>
<td><div class="cell">{{ sysFile.free }}</div></td> </div>
<td><div class="cell">{{ sysFile.used }}</div></td>
<td><div class="cell" :class="{'text-danger': sysFile.usage > 80}">{{ sysFile.usage }}%</div></td>
</tr>
</tbody>
</table>
</div>
</el-card>
</el-col>
</el-row>
</div> </div>
</template> </template>
<script> <script>
import { getServer } from "@/api/vadmin/monitor/server"; import {
cleanMonitorLog,
getMonitorLogs,
getMonitorStatusInfo,
getServerLatestLog,
getServerList,
updateMonitorStatusInfo,
updateServerInfo
} from '@/api/vadmin/monitor/server'
import InstrumentBoard from '@/views/vadmin/monitor/server/components/InstrumentBoard'
import LineChart from '@/views/vadmin/monitor/server/components/LineChart'
import moment from 'moment'
const debounce = require('lodash/debounce')
// key -> name
const SERVER_KEY_TO_NAME_MAPPING = {
ip: '服务器IP',
name: '服务器名称',
os: '操作系统',
remark: '备注'
}
//
const INTERVAL_ID_TO_TYPE_MAPPING = {
0: {
type: 0,
name: '秒',
key: 'seconds',
second: 1
},
1: {
type: 1,
name: '分钟',
key: 'minutes',
second: 60
},
2: {
type: 2,
name: '小时',
key: 'hours',
second: 60 * 60
},
3: {
type: 3,
name: '天',
key: 'days',
second: 24 * 60 * 60
}
}
const defaultUpdateInterval = INTERVAL_ID_TO_TYPE_MAPPING['0']
//
const CHART_KEY_NAME_MAPPING = {
cpu: 'CPU',
memory: '内存',
disk: '磁盘'
}
//
const INSTRUMENT_BOARD_KEY_TO_NAME_MAPPING = {
cpu: 'CPU使用率',
memory: '内存使用率'
}
//
const CHANGEABLE_SERVER_FIELDS = ['name', 'remark']
export default { export default {
name: "Server", name: 'Server',
components: {
InstrumentBoard,
LineChart
},
data() { data() {
return { return {
SERVER_KEY_TO_NAME_MAPPING,
INTERVAL_ID_TO_TYPE_MAPPING,
CHART_KEY_NAME_MAPPING,
CHANGEABLE_SERVER_FIELDS,
INSTRUMENT_BOARD_KEY_TO_NAME_MAPPING,
timeRange: [
`${moment().format('YYYY-MM-DD')} 00:00:00`,
`${moment().format('YYYY-MM-DD')} 23:59:59`
],
// //
loading: [], loading: [],
// //
server: [] allServerInfo: [],
}; //
currentServerName: '',
//
currentServer: {},
//
currentServerIndex: 0,
//
isOpeningMonitor: false,
//
monitorUpdateInterval: 60,
//
minMonitorUpdateInterval: 0,
//
intervalType: defaultUpdateInterval.name,
//
intervalTypeUnits: defaultUpdateInterval.second,
//
monitorLogSavingDays: 30,
// 线
lineChartData: {},
//
instrumentBoardData: {}
}
},
computed: {
currentServerInfoKeys() {
return Object.keys(this.currentServer).filter(key => {
if (SERVER_KEY_TO_NAME_MAPPING[key]) {
return { [key]: SERVER_KEY_TO_NAME_MAPPING[key] }
}
})
},
intervalNameToSecondMapping() {
let intervalNameToSecondMapping = {}
Object.values(INTERVAL_ID_TO_TYPE_MAPPING).forEach(item => {
intervalNameToSecondMapping[item.name] = item.second
})
return intervalNameToSecondMapping
},
monitorStatusInfo() {
return {
enabled: this.isOpeningMonitor ? 1 : 0,
save_days: this.monitorLogSavingDays,
interval: this.monitorUpdateInterval * this.intervalTypeUnits
}
}
},
watch: {
currentServer(newServerInfo) {
if (newServerInfo) {
//
this.getServerLatestLogInfo(newServerInfo.id)
//
this.getCurrentServerMonitorLogs()
}
}
}, },
created() { created() {
this.getList(); this.openLoading()
this.openLoading(); //
this.getServerList(this.currentServerIndex)
//
this.getMonitorStatusSettingsInfo()
}, },
methods: { methods: {
/** 查询服务器信息 */ /** 查询所有服务器基础信息 */
getList() { getServerList(serverIndex) {
getServer().then(response => { getServerList({ pageNum: 'all' }).then(response => {
this.server = response.data; this.allServerInfo = response.data
this.loading.close(); if (this.allServerInfo.length > 0) {
}); this.currentServer = this.allServerInfo[serverIndex || this.currentServerIndex]
this.currentServerName = this.currentServer.name
}
this.loading.close()
})
}, },
/**修改服务器信息*/
updateServerInfo() {
updateServerInfo(this.currentServer.id, this.currentServer).then(() => {
this.msgSuccess('修改服务器信息成功!')
}).catch(error => {
this.$message.error(error.msg || '提交修改服务器信息出错!')
}).finally(() => {
this.getServerList()
})
},
/** 获取服务器最新监控信息 */
getServerLatestLogInfo(serverId) {
getServerLatestLog(serverId).then(results => {
this.instrumentBoardData = results.data
}).catch(error => {
this.msgError(error.msg || '获取服务器最新监控信息错误!')
})
},
/** 获取监控日志信息 */
getCurrentServerMonitorLogs() {
getMonitorLogs(this.currentServer.id, { as: { 'create_datetime__range': this.timeRange } }).then(results => {
this.lineChartData = results.data
}).catch(error => {
this.msgError(error.msg || '获取监控日志信息出错误!')
})
},
/** 清除监控日志 */
cleanMonitorLogsInfo() {
this.$confirm('此操作将删除所有的监控记录,是否继续?', '提示', {
confirmButtonText: '确定删除',
cancelButtonText: '放弃'
}).then(() => {
cleanMonitorLog().then(results => {
this.msgSuccess( '清除记录成功!')
}).catch(error => {
this.$message.warning(error.msg || '清除记录失败,请重试!')
})
}).catch(() => {
})
},
/** 获取监控配置信息 */
getMonitorStatusSettingsInfo() {
getMonitorStatusInfo().then(results => {
let { enabled, interval, save_days } = results.data
this.isOpeningMonitor = enabled
this.monitorLogSavingDays = parseInt(save_days)
this.formatInterval(parseInt(interval))
}).catch(error => {
this.msgError(error.msg || '获取服务器监控配置信息出错误!')
})
},
/** 更新监控配置信息 */
updateMonitorStatusSettingsInfo() {
updateMonitorStatusInfo(this.monitorStatusInfo).then(() => {
this.msgSuccess('更新配置成功!')
}).catch((error) => {
this.msgError(error.msg || '更新服务器监控配置信息出错误!')
})
},
// //
openLoading() { openLoading() {
this.loading = this.$loading({ this.loading = this.$loading({
lock: true, lock: true,
text: "拼命读取中", text: '拼命读取中',
spinner: "el-icon-loading", spinner: 'el-icon-loading',
background: "rgba(0, 0, 0, 0.7)" background: 'rgba(0, 0, 0, 0.7)'
}); })
},
//
chooseServerInfo(index) {
this.currentServerIndex = index
this.currentServer = this.allServerInfo[index]
this.currentServerName = this.currentServer.name
},
//
handleIntervalChange: debounce(function(value) {
this.monitorUpdateInterval = value
}, 500),
//
selectIntervalType(value) {
this.intervalType = value
this.intervalTypeUnits = this.intervalNameToSecondMapping[value]
},
//
changeMonitorStatus(value) {
this.isOpeningMonitor = value
},
//
formatInterval(intervalTime) {
let biggerInterval = 0
for (let interval of Object.values(INTERVAL_ID_TO_TYPE_MAPPING)) {
if (interval.second > biggerInterval && interval.second < intervalTime) {
biggerInterval = interval.second
this.monitorUpdateInterval = intervalTime / interval.second
this.intervalType = interval.name
this.intervalTypeUnits = interval.second
}
}
} }
} }
}; }
</script> </script>
<style scoped>
.el-button--medium {
margin: 2px;
padding: 10px 10px;
}
.server-monitor-top {
padding: 0 10px;
text-align: justify-all;
}
.server-monitor-bottom {
text-align: left;
}
.server-information {
width: 20%;
min-width: 400px;
min-height: 300px;
display: inline-block;
}
.information-instrument-panel {
display: inline-block;
min-height: 300px;
min-width: 400px;
margin: 0 10px;
}
.server-info-item {
display: inline-block;
margin: 0 5px;
}
.server-info-detail {
min-height: 200px;
}
.server-info-detail-line {
margin: 5px;
min-height: 20px;
}
.server-info-detail-item {
text-align: justify;
line-height: 40px;
margin: 4px 0;
overflow: auto;
}
.server-monitor-control {
width: 100%;
height: 60px;
line-height: 60px;
padding: 0 20px;
}
.monitor-update-interval {
margin: 0 20px;
}
.same-block {
display: inline-block;
}
.monitor-update-interval-blank {
width: 100px;
margin: 0 2px;
}
.monitor-update-interval-unit {
width: 80px;
margin: 0 2px;
}
.monitor-log-save-time {
width: 280px;
margin: 0 2px;
}
.clean-monitor-log {
}
.server-monitor-line-chart {
height: 400px;
width: 45%;
min-width: 500px;
margin: 10px;
display: inline-block;
}
</style>

View File

@ -161,7 +161,7 @@
<i class="el-icon-arrow-down el-icon--right"></i> <i class="el-icon-arrow-down el-icon--right"></i>
</span> </span>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="(role,index) in scope.row.role" :key="index">{{role.roleName}}</el-dropdown-item> <el-dropdown-item v-for="role in scope.row.role">{{role.roleName}}</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
</template> </template>
@ -576,7 +576,7 @@
password: undefined, password: undefined,
mobile: undefined, mobile: undefined,
email: undefined, email: undefined,
gender: this.selectDictDefault(this.sexOptions), gender: undefined,
is_active: false, is_active: false,
remark: undefined, remark: undefined,
postIds: [], postIds: [],
@ -648,7 +648,6 @@
this.$refs['form'].validate(valid => { this.$refs['form'].validate(valid => {
if (valid) { if (valid) {
if (this.form.id != undefined) { if (this.form.id != undefined) {
this.form.creator = undefined
updateUser(this.form).then(response => { updateUser(this.form).then(response => {
this.msgSuccess('修改成功') this.msgSuccess('修改成功')
this.open = false this.open = false