diff --git a/db.sqlite3 b/db.sqlite3 index 70bf373..1b51f20 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/status/__pycache__/models.cpython-38.pyc b/status/__pycache__/models.cpython-38.pyc index 54831d7..666250e 100644 Binary files a/status/__pycache__/models.cpython-38.pyc and b/status/__pycache__/models.cpython-38.pyc differ diff --git a/status/__pycache__/urls.cpython-38.pyc b/status/__pycache__/urls.cpython-38.pyc index e303279..1588a2a 100644 Binary files a/status/__pycache__/urls.cpython-38.pyc and b/status/__pycache__/urls.cpython-38.pyc differ diff --git a/status/__pycache__/views.cpython-38.pyc b/status/__pycache__/views.cpython-38.pyc index 0f47acc..fc50f99 100644 Binary files a/status/__pycache__/views.cpython-38.pyc and b/status/__pycache__/views.cpython-38.pyc differ diff --git a/status/api_urls.py b/status/api_urls.py new file mode 100644 index 0000000..9f6731f --- /dev/null +++ b/status/api_urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from . import api_views + +urlpatterns = [ + # 客户端上报接口 + path('checkin/', api_views.CheckInAPIView.as_view(), name='api-checkin'), + + # 查询接口 + path('services/', api_views.ServiceListAPIView.as_view(), name='api-service-list'), + path('services//history/', api_views.ServiceHistoryAPIView.as_view(), name='api-service-history'), + path('status-summary/', api_views.StatusSummaryAPIView.as_view(), name='api-status-summary'), +] \ No newline at end of file diff --git a/status/api_views.py b/status/api_views.py new file mode 100644 index 0000000..05ca42c --- /dev/null +++ b/status/api_views.py @@ -0,0 +1,142 @@ +from rest_framework import status, generics +from rest_framework.views import APIView +from rest_framework.response import Response +from django.db.models import Count, Q +from .models import Service, ServiceCheckRecord, ServiceGroup +from .serializers import ( + ServiceSerializer, + ServiceCheckRecordSerializer, + ServiceCheckInSerializer, + StatusSummarySerializer +) + +# 引入loguru库用于日志记录 +try: + from loguru import logger +except ImportError: + import logging + logger = logging.getLogger(__name__) + + +class CheckInAPIView(APIView): + """客户端上报接口""" + + def post(self, request, *args, **kwargs): + """处理客户端上报请求""" + serializer = ServiceCheckInSerializer(data=request.data) + + if serializer.is_valid(): + try: + result = serializer.create(serializer.validated_data) + logger.info(f"服务上报成功,服务ID: {result['service_id']}") + return Response({ + "code": status.HTTP_200_OK, + "message": "上报成功", + "service_id": result['service_id'] + }) + except Exception as e: + logger.error(f"服务上报失败: {str(e)}") + return Response({ + "code": status.HTTP_500_INTERNAL_SERVER_ERROR, + "message": f"上报失败: {str(e)}" + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + else: + logger.warning(f"服务上报数据无效: {serializer.errors}") + return Response({ + "code": status.HTTP_400_BAD_REQUEST, + "message": "数据无效", + "errors": serializer.errors + }, status=status.HTTP_400_BAD_REQUEST) + + +class ServiceListAPIView(generics.ListAPIView): + """获取所有服务列表(含最新状态)""" + serializer_class = ServiceSerializer + queryset = Service.objects.all().select_related('group') + + def get_queryset(self): + queryset = super().get_queryset() + + # 支持按分组过滤 + group_name = self.request.query_params.get('group') + if group_name: + queryset = queryset.filter(group__name=group_name) + + # 支持按状态过滤 + status_filter = self.request.query_params.get('status') + if status_filter: + # 获取每个服务的最新状态 + service_ids_with_status = [] + for service in queryset: + if service.get_latest_status() == status_filter.upper(): + service_ids_with_status.append(service.id) + queryset = queryset.filter(id__in=service_ids_with_status) + + # 支持按名称搜索 + search = self.request.query_params.get('search') + if search: + queryset = queryset.filter( + Q(name__icontains=search) | + Q(host__icontains=search) | + Q(description__icontains=search) + ) + + logger.info(f"获取服务列表,过滤参数: group={group_name}, status={status_filter}, search={search}") + return queryset + + +class ServiceHistoryAPIView(generics.ListAPIView): + """获取某服务历史记录(分页)""" + serializer_class = ServiceCheckRecordSerializer + + def get_queryset(self): + service_id = self.kwargs.get('service_id') + try: + service = Service.objects.get(id=service_id) + except Service.DoesNotExist: + logger.warning(f"请求的服务不存在,ID: {service_id}") + return ServiceCheckRecord.objects.none() + + queryset = ServiceCheckRecord.objects.filter(service=service) + logger.info(f"获取服务历史记录,服务ID: {service_id}") + return queryset + + +class StatusSummaryAPIView(APIView): + """获取全局状态摘要""" + + def get(self, request, *args, **kwargs): + """获取全局状态摘要""" + services = Service.objects.all() + total_services = services.count() + + up_services = 0 + down_services = 0 + unknown_services = 0 + + for service in services: + latest_status = service.get_latest_status() + if latest_status == 'UP': + up_services += 1 + elif latest_status == 'DOWN': + down_services += 1 + else: + unknown_services += 1 + + # 计算正常运行时间百分比 + if total_services > 0: + uptime_percentage = (up_services / total_services) * 100 + else: + uptime_percentage = 0 + + data = { + 'total_services': total_services, + 'up_services': up_services, + 'down_services': down_services, + 'unknown_services': unknown_services, + 'uptime_percentage': round(uptime_percentage, 2) + } + + serializer = StatusSummarySerializer(data) + logger.info(f"获取状态摘要: {data}") + return Response(serializer.data) \ No newline at end of file diff --git a/status/models.py b/status/models.py index fef97af..057c210 100644 --- a/status/models.py +++ b/status/models.py @@ -43,6 +43,27 @@ class Service(models.Model): def __str__(self): return f"{self.name} ({self.host})" + def get_latest_status(self): + """获取最新状态""" + latest_record = self.records.first() + if latest_record: + return latest_record.status + return 'UNKNOWN' + + def get_latest_check_time(self): + """获取最新检测时间""" + latest_record = self.records.first() + if latest_record: + return latest_record.checked_at + return None + + def get_latest_response_time(self): + """获取最新响应时间""" + latest_record = self.records.first() + if latest_record: + return latest_record.response_time + return None + class Meta: verbose_name = '服务' verbose_name_plural = '服务' diff --git a/status/serializers.py b/status/serializers.py new file mode 100644 index 0000000..4ed77e0 --- /dev/null +++ b/status/serializers.py @@ -0,0 +1,96 @@ +from rest_framework import serializers +from .models import ServiceGroup, Service, ServiceCheckRecord + +# 引入loguru库用于日志记录 +try: + from loguru import logger +except ImportError: + import logging + logger = logging.getLogger(__name__) + + +class ServiceGroupSerializer(serializers.ModelSerializer): + class Meta: + model = ServiceGroup + fields = '__all__' + + +class ServiceCheckRecordSerializer(serializers.ModelSerializer): + class Meta: + model = ServiceCheckRecord + fields = '__all__' + + +class ServiceSerializer(serializers.ModelSerializer): + latest_status = serializers.CharField(source='get_latest_status', read_only=True) + latest_check_time = serializers.DateTimeField(source='get_latest_check_time', read_only=True) + latest_response_time = serializers.FloatField(source='get_latest_response_time', read_only=True) + + class Meta: + model = Service + fields = '__all__' + + +class ServiceCheckInSerializer(serializers.Serializer): + """客户端上报数据序列化器""" + service_name = serializers.CharField(max_length=200) + host = serializers.CharField(max_length=255) + port = serializers.IntegerField(required=False, allow_null=True) + check_type = serializers.CharField(max_length=50, default='tcp') + status = serializers.ChoiceField(choices=Service.STATUS_CHOICES) + response_time = serializers.FloatField(required=False, allow_null=True) + message = serializers.CharField(required=False, allow_blank=True) + + def create(self, validated_data): + """创建或更新服务记录""" + service_name = validated_data['service_name'] + host = validated_data['host'] + port = validated_data.get('port') + check_type = validated_data.get('check_type', 'tcp') + status = validated_data['status'] + response_time = validated_data.get('response_time') + message = validated_data.get('message', '') + + logger.info(f"收到服务上报: {service_name} ({host}:{port}), 状态: {status}") + + # 获取或创建默认分组 + group_name = 'Default' + group, created = ServiceGroup.objects.get_or_create(name=group_name) + if created: + logger.info(f"创建默认分组: {group_name}") + + # 获取或创建服务 + service, created = Service.objects.get_or_create( + name=service_name, + host=host, + port=port, + check_type=check_type, + defaults={'group': group} + ) + + if created: + logger.info(f"创建新服务: {service_name} ({host}:{port})") + + # 创建检测记录 + record = ServiceCheckRecord.objects.create( + service=service, + status=status, + response_time=response_time, + message=message + ) + + logger.info(f"保存检测记录: {service_name}, 状态: {status}, 响应时间: {response_time}ms") + + return { + 'service_id': service.id, + 'record_id': record.id + } + + +class StatusSummarySerializer(serializers.Serializer): + """状态摘要序列化器""" + total_services = serializers.IntegerField() + up_services = serializers.IntegerField() + down_services = serializers.IntegerField() + unknown_services = serializers.IntegerField() + uptime_percentage = serializers.FloatField() \ No newline at end of file diff --git a/status/templates/status/api_docs.html b/status/templates/status/api_docs.html new file mode 100644 index 0000000..4306815 --- /dev/null +++ b/status/templates/status/api_docs.html @@ -0,0 +1,130 @@ +{% extends 'status/base.html' %} + +{% block title %}API说明{% endblock %} + +{% block content %} +
+

API接口说明

+

本系统提供以下RESTful API接口,用于服务状态的上报和查询。

+ +
+

认证方式

+
+

目前所有API接口均无需认证,可直接访问。后续版本可能会添加认证机制。

+
+
+ +
+

API端点列表

+ +
+

POST /api/checkin/ - 客户端上报接口

+

用于客户端上报服务状态信息。如果服务不存在,系统会自动创建。

+ +
+

请求体示例:

+
{
+  "service_name": "Web服务",
+  "host": "192.168.1.100",
+  "port": 80,
+  "check_type": "http",
+  "status": "UP",
+  "response_time": 120.5,
+  "message": "连接成功"
+}
+
+ +
+

响应示例:

+
{
+  "code": 200,
+  "message": "服务状态已更新",
+  "service_id": 1
+}
+
+
+ +
+

GET /api/services/ - 服务列表查询接口

+

获取所有服务的列表及其最新状态。

+ +
+

响应示例:

+
[
+  {
+    "id": 1,
+    "name": "Web服务",
+    "host": "192.168.1.100",
+    "port": 80,
+    "check_type": "http",
+    "group": "Web服务组",
+    "description": "公司主网站",
+    "latest_status": "UP",
+    "latest_check_time": "2025-06-15T10:30:00Z",
+    "latest_response_time": 120.5
+  }
+]
+
+
+ +
+

GET /api/services/{id}/history/ - 服务历史记录查询接口

+

获取指定服务的历史检测记录。

+ +
+

响应示例:

+
{
+  "count": 1,
+  "total_pages": 1,
+  "current_page": 1,
+  "page_size": 20,
+  "results": [
+    {
+      "id": 1,
+      "service": 1,
+      "status": "UP",
+      "response_time": 120.5,
+      "message": "连接成功",
+      "check_time": "2025-06-15T10:30:00Z"
+    }
+  ]
+}
+
+
+ +
+

GET /api/status-summary/ - 状态摘要查询接口

+

获取所有服务的状态摘要统计信息。

+ +
+

响应示例:

+
{
+  "total_services": 10,
+  "up_count": 8,
+  "down_count": 1,
+  "unknown_count": 1
+}
+
+
+
+ +
+

状态码说明

+
    +
  • UP: 服务正常运行
  • +
  • DOWN: 服务不可用
  • +
  • UNKNOWN: 服务状态未知
  • +
+
+ +
+

检测类型说明

+
    +
  • http: HTTP检测
  • +
  • https: HTTPS检测
  • +
  • tcp: TCP端口检测
  • +
  • ping: PING检测
  • +
+
+
+{% endblock %} \ No newline at end of file diff --git a/status/templates/status/index.html b/status/templates/status/index.html index f3736ee..ac73009 100644 --- a/status/templates/status/index.html +++ b/status/templates/status/index.html @@ -56,9 +56,17 @@

服务状态监控

- - 最后更新: 加载中... -
+ + 管理后台 + + + API说明 + + +
+ +最后更新: 加载中... +
查看所有服务 diff --git a/status/urls.py b/status/urls.py index 78abbca..2129f87 100644 --- a/status/urls.py +++ b/status/urls.py @@ -19,6 +19,9 @@ urlpatterns = [ path('api/services/', views.api_services, name='api_services'), path('api/services//history/', views.api_service_history, name='api_service_history'), path('api/status-summary/', views.api_status_summary, name='api_status_summary'), + + # API文档页面 + path('api-docs/', views.api_docs, name='api_docs'), ] logger.info("URL路由配置已加载") \ No newline at end of file diff --git a/status/views.py b/status/views.py index f34cf84..7f78e23 100644 --- a/status/views.py +++ b/status/views.py @@ -217,3 +217,8 @@ def api_status_summary(request): """获取全局状态摘要(如:总共服务数、正常数、异常数)""" summary = get_status_summary() return JsonResponse(summary) + +# API文档页面 +def api_docs(request): + """API文档页面""" + return render(request, 'status/api_docs.html') diff --git a/statuspage/__pycache__/urls.cpython-38.pyc b/statuspage/__pycache__/urls.cpython-38.pyc index 938d594..20813f0 100644 Binary files a/statuspage/__pycache__/urls.cpython-38.pyc and b/statuspage/__pycache__/urls.cpython-38.pyc differ diff --git a/statuspage/urls.py b/statuspage/urls.py index 123b6d1..24e1c53 100644 --- a/statuspage/urls.py +++ b/statuspage/urls.py @@ -20,4 +20,5 @@ from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('status.urls')), + path('api/', include('status.api_urls')), ]