重新架构了整个models
This commit is contained in:
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
@@ -1 +1,3 @@
|
|||||||
Django==4.2.7
|
Django==4.2.7
|
||||||
|
djangorestframework==3.14.0
|
||||||
|
loguru==0.7.2
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,9 +1,50 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Service
|
from .models import ServiceGroup, Service, ServiceCheckRecord
|
||||||
|
|
||||||
|
# 引入loguru库用于日志记录
|
||||||
|
try:
|
||||||
|
from loguru import logger
|
||||||
|
except ImportError:
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@admin.register(ServiceGroup)
|
||||||
|
class ServiceGroupAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'description')
|
||||||
|
search_fields = ('name', 'description')
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
if change:
|
||||||
|
logger.info(f"更新服务分组: {obj.name}")
|
||||||
|
else:
|
||||||
|
logger.info(f"创建新服务分组: {obj.name}")
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
@admin.register(Service)
|
||||||
class ServiceAdmin(admin.ModelAdmin):
|
class ServiceAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'status', 'description', 'ip_address', 'port', 'reliability', 'last_updated')
|
list_display = ('name', 'group', 'host', 'port', 'check_type', 'is_active', 'created_at')
|
||||||
list_filter = ('status',)
|
list_filter = ('group', 'check_type', 'is_active')
|
||||||
search_fields = ('name', 'ip_address', 'description')
|
search_fields = ('name', 'host', 'description')
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
if change:
|
||||||
|
logger.info(f"更新服务: {obj.name} ({obj.host})")
|
||||||
|
else:
|
||||||
|
logger.info(f"创建新服务: {obj.name} ({obj.host})")
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
admin.site.register(Service, ServiceAdmin)
|
@admin.register(ServiceCheckRecord)
|
||||||
|
class ServiceCheckRecordAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('service', 'status', 'response_time', 'checked_at')
|
||||||
|
list_filter = ('status', 'service__group', 'service')
|
||||||
|
search_fields = ('service__name', 'message')
|
||||||
|
date_hierarchy = 'checked_at'
|
||||||
|
readonly_fields = ('service', 'status', 'response_time', 'message', 'checked_at')
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
return False # 禁止手动添加检测记录,只能通过API上报
|
||||||
|
|
||||||
|
def has_change_permission(self, request, obj=None):
|
||||||
|
return False # 禁止修改检测记录
|
||||||
|
|
||||||
|
logger.info("管理后台配置已加载")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Generated by Django 4.2.7 on 2025-06-16 12:51
|
# Generated by Django 4.2.7 on 2025-09-07 08:41
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
@@ -16,17 +17,50 @@ class Migration(migrations.Migration):
|
|||||||
name='Service',
|
name='Service',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(max_length=100, verbose_name='服务名称')),
|
('name', models.CharField(max_length=200, verbose_name='服务名称')),
|
||||||
('status', models.CharField(choices=[('operational', '正常运行'), ('degraded', '性能下降'), ('outage', '服务中断')], default='operational', max_length=20, verbose_name='服务状态')),
|
('host', models.CharField(max_length=255, verbose_name='主机/IP')),
|
||||||
('description', models.TextField(verbose_name='服务描述')),
|
('port', models.PositiveIntegerField(blank=True, null=True, verbose_name='端口(可选)')),
|
||||||
('ip_address', models.GenericIPAddressField(verbose_name='IP地址')),
|
('check_type', models.CharField(choices=[('ping', 'Ping检测'), ('tcp', 'TCP端口检测'), ('http', 'HTTP请求'), ('custom', '自定义脚本')], default='ping', max_length=50, verbose_name='检测类型')),
|
||||||
('port', models.PositiveIntegerField(verbose_name='端口号')),
|
('description', models.TextField(blank=True, null=True, verbose_name='描述')),
|
||||||
('reliability', models.DecimalField(decimal_places=2, default=99.0, max_digits=5, verbose_name='可靠率(%)')),
|
('is_active', models.BooleanField(default=True, verbose_name='是否启用监控')),
|
||||||
('last_updated', models.DateTimeField(default=django.utils.timezone.now, verbose_name='最后更新时间')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': '服务状态',
|
'verbose_name': '服务',
|
||||||
'verbose_name_plural': '服务状态',
|
'verbose_name_plural': '服务',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ServiceGroup',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, unique=True, verbose_name='服务分组')),
|
||||||
|
('description', models.TextField(blank=True, null=True, verbose_name='描述')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '服务分组',
|
||||||
|
'verbose_name_plural': '服务分组',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ServiceCheckRecord',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('status', models.CharField(choices=[('UP', '正常'), ('DOWN', '故障'), ('UNKNOWN', '未知')], max_length=10, verbose_name='状态')),
|
||||||
|
('response_time', models.FloatField(blank=True, null=True, verbose_name='响应时间(ms)')),
|
||||||
|
('message', models.TextField(blank=True, null=True, verbose_name='返回信息/错误原因')),
|
||||||
|
('checked_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='检测时间')),
|
||||||
|
('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records', to='status.service')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '服务检测记录',
|
||||||
|
'verbose_name_plural': '服务检测记录',
|
||||||
|
'ordering': ['-checked_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='service',
|
||||||
|
name='group',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='services', to='status.servicegroup', verbose_name='所属分组'),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
Binary file not shown.
@@ -1,24 +1,67 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
class Service(models.Model):
|
# 引入loguru库用于日志记录
|
||||||
STATUS_CHOICES = (
|
try:
|
||||||
('operational', '正常运行'),
|
from loguru import logger
|
||||||
('degraded', '性能下降'),
|
except ImportError:
|
||||||
('outage', '服务中断'),
|
import logging
|
||||||
)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
name = models.CharField(max_length=100, verbose_name='服务名称')
|
class ServiceGroup(models.Model):
|
||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='operational', verbose_name='服务状态')
|
name = models.CharField(max_length=100, unique=True, verbose_name="服务分组")
|
||||||
description = models.TextField(verbose_name='服务描述')
|
description = models.TextField(blank=True, null=True, verbose_name="描述")
|
||||||
ip_address = models.GenericIPAddressField(verbose_name='IP地址')
|
|
||||||
port = models.PositiveIntegerField(verbose_name='端口号')
|
|
||||||
reliability = models.DecimalField(max_digits=5, decimal_places=2, default=99.00, verbose_name='可靠率(%)')
|
|
||||||
last_updated = models.DateTimeField(default=timezone.now, verbose_name='最后更新时间')
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = '服务状态'
|
verbose_name = '服务分组'
|
||||||
verbose_name_plural = '服务状态'
|
verbose_name_plural = '服务分组'
|
||||||
|
|
||||||
|
class Service(models.Model):
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('UP', '正常'),
|
||||||
|
('DOWN', '故障'),
|
||||||
|
('UNKNOWN', '未知'),
|
||||||
|
]
|
||||||
|
|
||||||
|
name = models.CharField(max_length=200, verbose_name="服务名称")
|
||||||
|
group = models.ForeignKey(ServiceGroup, on_delete=models.CASCADE, related_name='services', verbose_name="所属分组")
|
||||||
|
host = models.CharField(max_length=255, verbose_name="主机/IP")
|
||||||
|
port = models.PositiveIntegerField(null=True, blank=True, verbose_name="端口(可选)")
|
||||||
|
check_type = models.CharField(max_length=50, default='ping', choices=[
|
||||||
|
('ping', 'Ping检测'),
|
||||||
|
('tcp', 'TCP端口检测'),
|
||||||
|
('http', 'HTTP请求'),
|
||||||
|
('custom', '自定义脚本'),
|
||||||
|
], verbose_name="检测类型")
|
||||||
|
description = models.TextField(blank=True, null=True, verbose_name="描述")
|
||||||
|
is_active = models.BooleanField(default=True, verbose_name="是否启用监控")
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} ({self.host})"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = '服务'
|
||||||
|
verbose_name_plural = '服务'
|
||||||
|
|
||||||
|
class ServiceCheckRecord(models.Model):
|
||||||
|
service = models.ForeignKey(Service, on_delete=models.CASCADE, related_name='records')
|
||||||
|
status = models.CharField(max_length=10, choices=Service.STATUS_CHOICES, verbose_name="状态")
|
||||||
|
response_time = models.FloatField(null=True, blank=True, verbose_name="响应时间(ms)")
|
||||||
|
message = models.TextField(blank=True, null=True, verbose_name="返回信息/错误原因")
|
||||||
|
checked_at = models.DateTimeField(default=timezone.now, verbose_name="检测时间")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-checked_at']
|
||||||
|
verbose_name = '服务检测记录'
|
||||||
|
verbose_name_plural = '服务检测记录'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.service.name} - {self.status} at {self.checked_at}"
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
logger.info(f"保存服务检测记录: {self.service.name}, 状态: {self.status}, 响应时间: {self.response_time}ms")
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>服务状态监控</title>
|
<title>服务状态监控</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||||
<script>
|
<script>
|
||||||
tailwind.config = {
|
tailwind.config = {
|
||||||
theme: {
|
theme: {
|
||||||
@@ -52,13 +52,18 @@
|
|||||||
<div class="container mx-auto px-4 py-6">
|
<div class="container mx-auto px-4 py-6">
|
||||||
<div class="flex flex-col md:flex-row justify-between items-center">
|
<div class="flex flex-col md:flex-row justify-between items-center">
|
||||||
<div class="flex items-center mb-4 md:mb-0">
|
<div class="flex items-center mb-4 md:mb-0">
|
||||||
<i class="fa fa-server text-3xl mr-3"></i>
|
<i class="fas fa-server text-3xl mr-3"></i>
|
||||||
<h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold">服务状态监控</h1>
|
<h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold">服务状态监控</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<i class="fa fa-refresh animate-spin mr-2"></i>
|
<i class="fas fa-refresh animate-spin mr-2"></i>
|
||||||
<span id="last-updated" class="text-sm md:text-base">最后更新: 加载中...</span>
|
<span id="last-updated" class="text-sm md:text-base">最后更新: 加载中...</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-4 md:mt-0">
|
||||||
|
<a href="/services/" class="bg-white text-primary px-4 py-2 rounded-lg font-medium hover:bg-gray-100 transition-colors">
|
||||||
|
<i class="fas fa-list mr-2"></i> 查看所有服务
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-gray-200">实时监控系统服务运行状态与可靠性</p>
|
<p class="mt-2 text-gray-200">实时监控系统服务运行状态与可靠性</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,43 +76,72 @@
|
|||||||
<div class="bg-white rounded-xl p-6 card-shadow card-hover border-l-4 border-operational">
|
<div class="bg-white rounded-xl p-6 card-shadow card-hover border-l-4 border-operational">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-500 text-sm">正常运行</p>
|
<p class="text-gray-500 text-sm">正常</p>
|
||||||
<h3 id="operational-count" class="text-3xl font-bold mt-1">0</h3>
|
<h3 id="operational-count" class="text-3xl font-bold mt-1">0</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-12 h-12 rounded-full bg-operational/10 flex items-center justify-center text-operational">
|
<div class="w-12 h-12 rounded-full bg-operational/10 flex items-center justify-center text-operational">
|
||||||
<i class="fa fa-check-circle text-xl"></i>
|
<i class="fas fa-check-circle text-xl"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white rounded-xl p-6 card-shadow card-hover border-l-4 border-degraded">
|
<div class="bg-white rounded-xl p-6 card-shadow card-hover border-l-4 border-degraded">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-500 text-sm">性能下降</p>
|
<p class="text-gray-500 text-sm">未知</p>
|
||||||
<h3 id="degraded-count" class="text-3xl font-bold mt-1">0</h3>
|
<h3 id="degraded-count" class="text-3xl font-bold mt-1">0</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-12 h-12 rounded-full bg-degraded/10 flex items-center justify-center text-degraded">
|
<div class="w-12 h-12 rounded-full bg-degraded/10 flex items-center justify-center text-degraded">
|
||||||
<i class="fa fa-exclamation-triangle text-xl"></i>
|
<i class="fas fa-question-circle text-xl"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white rounded-xl p-6 card-shadow card-hover border-l-4 border-outage">
|
<div class="bg-white rounded-xl p-6 card-shadow card-hover border-l-4 border-outage">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-500 text-sm">服务中断</p>
|
<p class="text-gray-500 text-sm">故障</p>
|
||||||
<h3 id="outage-count" class="text-3xl font-bold mt-1">0</h3>
|
<h3 id="outage-count" class="text-3xl font-bold mt-1">0</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-12 h-12 rounded-full bg-outage/10 flex items-center justify-center text-outage">
|
<div class="w-12 h-12 rounded-full bg-outage/10 flex items-center justify-center text-outage">
|
||||||
<i class="fa fa-times-circle text-xl"></i>
|
<i class="fas fa-times-circle text-xl"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 最近异常 -->
|
||||||
|
<div class="bg-white rounded-xl p-6 card-shadow mb-10">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h2 class="text-xl font-bold text-gray-800">最近异常</h2>
|
||||||
|
<a href="/services/?status=down" class="text-primary hover:underline text-sm font-medium">
|
||||||
|
查看全部 <i class="fas fa-arrow-right ml-1"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">服务名称</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">消息</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">检测时间</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200" id="recent-issues-tbody">
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="px-6 py-4 text-center text-gray-500">
|
||||||
|
<i class="fas fa-circle-o-notch fa-spin mr-2"></i> 加载中...
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 服务列表 -->
|
<!-- 服务列表 -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" id="services-container">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" id="services-container">
|
||||||
<!-- 服务卡片将通过JavaScript动态生成 -->
|
<!-- 服务卡片将通过JavaScript动态生成 -->
|
||||||
<div class="col-span-full text-center py-12 text-gray-500">
|
<div class="col-span-full text-center py-12 text-gray-500">
|
||||||
<i class="fa fa-circle-o-notch fa-spin text-3xl mb-4"></i>
|
<i class="fas fa-circle-o-notch fa-spin text-3xl mb-4"></i>
|
||||||
<p>加载服务数据中...</p>
|
<p>加载服务数据中...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,20 +157,20 @@
|
|||||||
<script>
|
<script>
|
||||||
// 状态样式映射
|
// 状态样式映射
|
||||||
const statusStyles = {
|
const statusStyles = {
|
||||||
operational: {
|
up: {
|
||||||
class: 'bg-operational text-white',
|
class: 'bg-operational text-white',
|
||||||
icon: 'fa-check-circle',
|
icon: 'fas fa-check-circle',
|
||||||
text: '正常运行'
|
text: '正常'
|
||||||
},
|
},
|
||||||
degraded: {
|
down: {
|
||||||
class: 'bg-degraded text-white',
|
|
||||||
icon: 'fa-exclamation-triangle',
|
|
||||||
text: '性能下降'
|
|
||||||
},
|
|
||||||
outage: {
|
|
||||||
class: 'bg-outage text-white',
|
class: 'bg-outage text-white',
|
||||||
icon: 'fa-times-circle',
|
icon: 'fas fa-times-circle',
|
||||||
text: '服务中断'
|
text: '故障'
|
||||||
|
},
|
||||||
|
unknown: {
|
||||||
|
class: 'bg-degraded text-white',
|
||||||
|
icon: 'fas fa-question-circle',
|
||||||
|
text: '未知'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,19 +185,23 @@
|
|||||||
|
|
||||||
// 统计各状态数量
|
// 统计各状态数量
|
||||||
const counts = {
|
const counts = {
|
||||||
operational: 0,
|
up: 0,
|
||||||
degraded: 0,
|
down: 0,
|
||||||
outage: 0
|
unknown: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
data.forEach(service => {
|
data.forEach(service => {
|
||||||
counts[service.status]++;
|
if (service.latest_status) {
|
||||||
|
counts[service.latest_status.toLowerCase()]++;
|
||||||
|
} else {
|
||||||
|
counts.unknown++;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新统计数字
|
// 更新统计数字
|
||||||
document.getElementById('operational-count').textContent = counts.operational;
|
document.getElementById('operational-count').textContent = counts.up;
|
||||||
document.getElementById('degraded-count').textContent = counts.degraded;
|
document.getElementById('degraded-count').textContent = counts.unknown;
|
||||||
document.getElementById('outage-count').textContent = counts.outage;
|
document.getElementById('outage-count').textContent = counts.down;
|
||||||
|
|
||||||
// 生成服务卡片
|
// 生成服务卡片
|
||||||
const container = document.getElementById('services-container');
|
const container = document.getElementById('services-container');
|
||||||
@@ -172,7 +210,7 @@
|
|||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="col-span-full text-center py-12 text-gray-500">
|
<div class="col-span-full text-center py-12 text-gray-500">
|
||||||
<i class="fa fa-info-circle text-3xl mb-4"></i>
|
<i class="fas fa-info-circle text-3xl mb-4"></i>
|
||||||
<p>暂无服务数据</p>
|
<p>暂无服务数据</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -180,7 +218,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
data.forEach(service => {
|
data.forEach(service => {
|
||||||
const style = statusStyles[service.status];
|
const status = service.latest_status ? service.latest_status.toLowerCase() : 'unknown';
|
||||||
|
const style = statusStyles[status];
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'bg-white rounded-xl overflow-hidden card-shadow card-hover';
|
card.className = 'bg-white rounded-xl overflow-hidden card-shadow card-hover';
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
@@ -188,29 +227,41 @@
|
|||||||
<div class="flex justify-between items-start mb-4">
|
<div class="flex justify-between items-start mb-4">
|
||||||
<h3 class="text-xl font-bold text-gray-800">${service.name}</h3>
|
<h3 class="text-xl font-bold text-gray-800">${service.name}</h3>
|
||||||
<span class="px-3 py-1 rounded-full text-sm font-medium ${style.class}">
|
<span class="px-3 py-1 rounded-full text-sm font-medium ${style.class}">
|
||||||
<i class="fa ${style.icon} mr-1"></i> ${style.text}
|
<i class="fas ${style.icon} mr-1"></i> ${style.text}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-600 mb-4">${service.description}</p>
|
<div class="text-sm text-gray-600 mb-2">
|
||||||
|
<span class="inline-block bg-gray-100 rounded-full px-3 py-1 text-xs font-medium text-gray-800 mr-2">
|
||||||
|
${service.group}
|
||||||
|
</span>
|
||||||
|
<span class="inline-block bg-gray-100 rounded-full px-3 py-1 text-xs font-medium text-gray-800">
|
||||||
|
${service.check_type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 mb-4">${service.description || '暂无描述'}</p>
|
||||||
<div class="grid grid-cols-2 gap-3 text-sm">
|
<div class="grid grid-cols-2 gap-3 text-sm">
|
||||||
<div class="bg-gray-50 p-3 rounded-lg">
|
<div class="bg-gray-50 p-3 rounded-lg">
|
||||||
<p class="text-gray-500">IP地址</p>
|
<p class="text-gray-500">主机/IP</p>
|
||||||
<p class="font-medium">${service.ip_address}</p>
|
<p class="font-medium">${service.host}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 p-3 rounded-lg">
|
<div class="bg-gray-50 p-3 rounded-lg">
|
||||||
<p class="text-gray-500">端口</p>
|
<p class="text-gray-500">端口</p>
|
||||||
<p class="font-medium">${service.port}</p>
|
<p class="font-medium">${service.port || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
|
${service.latest_response_time ? `
|
||||||
<div class="bg-gray-50 p-3 rounded-lg col-span-2">
|
<div class="bg-gray-50 p-3 rounded-lg col-span-2">
|
||||||
<p class="text-gray-500">可靠率</p>
|
<p class="text-gray-500">响应时间</p>
|
||||||
<div class="w-full bg-gray-200 rounded-full h-2 mt-1">
|
<p class="font-medium">${service.latest_response_time} ms</p>
|
||||||
<div class="${style.class} h-2 rounded-full" style="width: ${service.reliability}%"></div>
|
|
||||||
</div>
|
|
||||||
<p class="font-medium mt-1">${service.reliability}%</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 text-xs text-gray-500">
|
<div class="mt-4 text-xs text-gray-500">
|
||||||
<p>最后更新: ${new Date(service.last_updated).toLocaleString()}</p>
|
<p>最后检测: ${service.latest_check_time ? new Date(service.latest_check_time).toLocaleString() : '从未检测'}</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="/services/${service.id}/" class="text-primary hover:underline text-sm font-medium">
|
||||||
|
查看详情 <i class="fas fa-arrow-right ml-1"></i>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -222,7 +273,7 @@
|
|||||||
const container = document.getElementById('services-container');
|
const container = document.getElementById('services-container');
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="col-span-full text-center py-12 text-red-500">
|
<div class="col-span-full text-center py-12 text-red-500">
|
||||||
<i class="fa fa-exclamation-circle text-3xl mb-4"></i>
|
<i class="fas fa-exclamation-circle text-3xl mb-4"></i>
|
||||||
<p>加载服务数据失败,请刷新页面重试</p>
|
<p>加载服务数据失败,请刷新页面重试</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
340
status/templates/status/service_detail.html
Normal file
340
status/templates/status/service_detail.html
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-cn">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>服务详情 - 服务状态监控</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: '#165dff',
|
||||||
|
operational: '#36d399',
|
||||||
|
degraded: '#fbbd23',
|
||||||
|
outage: '#f87272',
|
||||||
|
dark: '#1e293b',
|
||||||
|
light: '#f8fafc'
|
||||||
|
},
|
||||||
|
fontfamily: {
|
||||||
|
inter: ['inter', 'system-ui', 'sans-serif'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style type="text/tailwindcss">
|
||||||
|
@layer utilities {
|
||||||
|
.content-auto {
|
||||||
|
content-visibility: auto;
|
||||||
|
}
|
||||||
|
.card-shadow {
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
.card-hover {
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translatey(-5px);
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
.gradient-bg {
|
||||||
|
background: linear-gradient(135deg, #165dff 0%, #0a2463 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-50 font-inter min-h-screen">
|
||||||
|
<!-- 顶部导航 -->
|
||||||
|
<header class="gradient-bg text-white shadow-lg">
|
||||||
|
<div class="container mx-auto px-4 py-6">
|
||||||
|
<div class="flex flex-col md:flex-row justify-between items-center">
|
||||||
|
<div class="flex items-center mb-4 md:mb-0">
|
||||||
|
<a href="/" class="flex items-center">
|
||||||
|
<i class="fas fa-server text-3xl mr-3"></i>
|
||||||
|
<h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold">服务状态监控</h1>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<a href="/" class="text-white hover:text-gray-200">
|
||||||
|
<i class="fas fa-home mr-2"></i>首页
|
||||||
|
</a>
|
||||||
|
<a href="/services/" class="text-white hover:text-gray-200">
|
||||||
|
<i class="fas fa-list mr-2"></i>服务列表
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<main class="container mx-auto px-4 py-8">
|
||||||
|
<!-- 服务基本信息 -->
|
||||||
|
<div class="bg-white rounded-xl p-6 card-shadow mb-8">
|
||||||
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-gray-800">{{ service.name }}</h2>
|
||||||
|
<div class="flex items-center mt-2">
|
||||||
|
{% if service.latest_status == 'UP' %}
|
||||||
|
<span class="px-3 inline-flex text-sm leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
||||||
|
<i class="fas fa-check-circle mr-1"></i> 正常
|
||||||
|
</span>
|
||||||
|
{% elif service.latest_status == 'DOWN' %}
|
||||||
|
<span class="px-3 inline-flex text-sm leading-5 font-semibold rounded-full bg-red-100 text-red-800">
|
||||||
|
<i class="fas fa-times-circle mr-1"></i> 故障
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="px-3 inline-flex text-sm leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
|
||||||
|
<i class="fas fa-question-circle mr-1"></i> 未知
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="ml-3 text-sm text-gray-500">
|
||||||
|
<i class="fas fa-clock-o mr-1"></i>
|
||||||
|
{% if service.latest_check_time %}最后检测: {{ service.latest_check_time|date:"y-m-d h:i:s" }}{% else %}从未检测{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="/services/" class="mt-4 md:mt-0 bg-gray-100 text-gray-700 py-2 px-4 rounded-lg hover:bg-gray-200 transition-colors">
|
||||||
|
<i class="fas fa-arrow-left mr-2"></i>返回服务列表
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-700 mb-3">基本信息</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex">
|
||||||
|
<span class="w-32 text-gray-500">服务名称:</span>
|
||||||
|
<span class="text-gray-800">{{ service.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<span class="w-32 text-gray-500">所属分组:</span>
|
||||||
|
<span class="text-gray-800">{{ service.group.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<span class="w-32 text-gray-500">主机/IP:</span>
|
||||||
|
<span class="text-gray-800">{{ service.host }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<span class="w-32 text-gray-500">端口:</span>
|
||||||
|
<span class="text-gray-800">{% if service.port %}{{ service.port }}{% else %}N/A{% endif %}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<span class="w-32 text-gray-500">检测类型:</span>
|
||||||
|
<span class="text-gray-800">{{ service.get_check_type_display }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<span class="w-32 text-gray-500">是否启用:</span>
|
||||||
|
<span class="text-gray-800">{% if service.is_active %}是{% else %}否{% endif %}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-700 mb-3">最新状态</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex">
|
||||||
|
<span class="w-32 text-gray-500">当前状态:</span>
|
||||||
|
<span class="text-gray-800">
|
||||||
|
{% if service.latest_status == 'UP' %}正常
|
||||||
|
{% elif service.latest_status == 'DOWN' %}故障
|
||||||
|
{% else %}未知{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<span class="w-32 text-gray-500">响应时间:</span>
|
||||||
|
<span class="text-gray-800">{% if service.latest_response_time %}{{ service.latest_response_time }} ms{% else %}N/A{% endif %}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<span class="w-32 text-gray-500">最后检测:</span>
|
||||||
|
<span class="text-gray-800">{% if service.latest_check_time %}{{ service.latest_check_time|date:"y-m-d h:i:s" }}{% else %}从未检测{% endif %}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<span class="w-32 text-gray-500">检测消息:</span>
|
||||||
|
<span class="text-gray-800">{% if service.latest_message %}{{ service.latest_message }}{% else %}无{% endif %}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if service.description %}
|
||||||
|
<div class="mt-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-700 mb-3">描述</h3>
|
||||||
|
<p class="text-gray-600">{{ service.description }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 响应时间趋势图 -->
|
||||||
|
<div class="bg-white rounded-xl p-6 card-shadow mb-8">
|
||||||
|
<h3 class="text-xl font-bold text-gray-800 mb-4">响应时间趋势 (最近24小时)</h3>
|
||||||
|
<div class="h-64">
|
||||||
|
<canvas id="response-time-chart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 状态变化时间轴 -->
|
||||||
|
<div class="bg-white rounded-xl p-6 card-shadow mb-8">
|
||||||
|
<h3 class="text-xl font-bold text-gray-800 mb-4">状态变化时间轴</h3>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute left-4 top-0 bottom-0 w-0.5 bg-gray-200"></div>
|
||||||
|
<div class="space-y-4" id="status-timeline">
|
||||||
|
{% for record in status_changes %}
|
||||||
|
<div class="relative pl-10">
|
||||||
|
<div class="absolute left-0 w-8 h-8 rounded-full flex items-center justify-center
|
||||||
|
{% if record.status == 'UP' %}bg-green-100 text-green-600{% elif record.status == 'DOWN' %}bg-red-100 text-red-600{% else %}bg-yellow-100 text-yellow-600{% endif %}
|
||||||
|
">
|
||||||
|
{% if record.status == 'UP' %}<i class="fas fa-check"></i>
|
||||||
|
{% elif record.status == 'DOWN' %}<i class="fas fa-times"></i>
|
||||||
|
{% else %}<i class="fas fa-question"></i>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="font-medium">
|
||||||
|
{% if record.status == 'UP' %}正常
|
||||||
|
{% elif record.status == 'DOWN' %}故障
|
||||||
|
{% else %}未知{% endif %}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm text-gray-500">{{ record.checked_at|date:"y-m-d h:i:s" }}</span>
|
||||||
|
</div>
|
||||||
|
{% if record.message %}
|
||||||
|
<p class="text-sm text-gray-600 mt-1">{{ record.message }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if record.response_time %}
|
||||||
|
<p class="text-sm text-gray-500 mt-1">响应时间: {{ record.response_time }} ms</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="text-center py-4 text-gray-500">
|
||||||
|
<i class="fas fa-info-circle text-2xl mb-2"></i>
|
||||||
|
<p>暂无状态变化记录</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 最近检测记录 -->
|
||||||
|
<div class="bg-white rounded-xl overflow-hidden card-shadow">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h3 class="text-xl font-bold text-gray-800">最近检测记录</h3>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">检测时间</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">响应时间</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">消息</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
{% for record in recent_records %}
|
||||||
|
<tr class="hover:bg-gray-50">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ record.checked_at|date:"y-m-d h:i:s" }}</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
{% if record.status == 'UP' %}
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
||||||
|
<i class="fas fa-check-circle mr-1"></i> 正常
|
||||||
|
</span>
|
||||||
|
{% elif record.status == 'DOWN' %}
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
|
||||||
|
<i class="fas fa-times-circle mr-1"></i> 故障
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
|
||||||
|
<i class="fas fa-question-circle mr-1"></i> 未知
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{% if record.response_time %}{{ record.response_time }} ms{% else %}N/A{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-500 max-w-md truncate">
|
||||||
|
{% if record.message %}{{ record.message }}{% else %}无{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="px-6 py-4 text-center text-gray-500">
|
||||||
|
<i class="fas fa-info-circle text-2xl mb-2"></i>
|
||||||
|
<p>暂无检测记录</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 页脚 -->
|
||||||
|
<footer class="bg-dark text-white py-6 mt-12">
|
||||||
|
<div class="container mx-auto px-4 text-center">
|
||||||
|
<p>© 2025 服务状态监控系统</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 响应时间趋势图
|
||||||
|
const ctx = document.getelementbyid('response-time-chart').getcontext('2d');
|
||||||
|
const responsetimechart = new chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: {{ chart_labels|safe }},
|
||||||
|
datasets: [{
|
||||||
|
label: '响应时间 (ms)',
|
||||||
|
data: {{ chart_data|safe }},
|
||||||
|
backgroundcolor: 'rgba(22, 93, 255, 0.1)',
|
||||||
|
bordercolor: '#165dff',
|
||||||
|
borderwidth: 2,
|
||||||
|
tension: 0.3,
|
||||||
|
pointbackgroundcolor: '#165dff',
|
||||||
|
pointbordercolor: '#fff',
|
||||||
|
pointborderwidth: 2,
|
||||||
|
pointradius: 4,
|
||||||
|
pointhoverradius: 6
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainaspectratio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
return `响应时间: ${context.parsed.y} ms`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginatzero: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: '响应时间 (ms)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: '时间'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
216
status/templates/status/service_list.html
Normal file
216
status/templates/status/service_list.html
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-cn">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>服务列表 - 服务状态监控</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: '#165dff',
|
||||||
|
operational: '#36d399',
|
||||||
|
degraded: '#fbbd23',
|
||||||
|
outage: '#f87272',
|
||||||
|
dark: '#1e293b',
|
||||||
|
light: '#f8fafc'
|
||||||
|
},
|
||||||
|
fontfamily: {
|
||||||
|
inter: ['inter', 'system-ui', 'sans-serif'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style type="text/tailwindcss">
|
||||||
|
@layer utilities {
|
||||||
|
.content-auto {
|
||||||
|
content-visibility: auto;
|
||||||
|
}
|
||||||
|
.card-shadow {
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
.card-hover {
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translatey(-5px);
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
.gradient-bg {
|
||||||
|
background: linear-gradient(135deg, #165dff 0%, #0a2463 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-50 font-inter min-h-screen">
|
||||||
|
<!-- 顶部导航 -->
|
||||||
|
<header class="gradient-bg text-white shadow-lg">
|
||||||
|
<div class="container mx-auto px-4 py-6">
|
||||||
|
<div class="flex flex-col md:flex-row justify-between items-center">
|
||||||
|
<div class="flex items-center mb-4 md:mb-0">
|
||||||
|
<a href="/" class="flex items-center">
|
||||||
|
<i class="fas fa-server text-3xl mr-3"></i>
|
||||||
|
<h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold">服务状态监控</h1>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<a href="/" class="text-white hover:text-gray-200">
|
||||||
|
<i class="fas fa-home mr-2"></i>首页
|
||||||
|
</a>
|
||||||
|
<a href="/services/" class="text-white font-medium border-b-2 border-white pb-1">
|
||||||
|
<i class="fas fa-list mr-2"></i>服务列表
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<main class="container mx-auto px-4 py-8">
|
||||||
|
<!-- 筛选和搜索 -->
|
||||||
|
<div class="bg-white rounded-xl p-6 card-shadow mb-8">
|
||||||
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||||
|
<h2 class="text-xl font-bold text-gray-800">服务列表</h2>
|
||||||
|
<div class="flex flex-col md:flex-row gap-3 w-full md:w-auto">
|
||||||
|
<div class="relative">
|
||||||
|
<select id="group-filter" class="appearance-none bg-gray-50 border border-gray-300 text-gray-700 py-2 px-4 pr-8 rounded-lg leading-tight focus:outline-none focus:bg-white focus:border-primary w-full md:w-auto">
|
||||||
|
<option value="">所有分组</option>
|
||||||
|
{% for group in groups %}
|
||||||
|
<option value="{{ group.name }}" {% if current_group == group.name %}selected{% endif %}>{{ group.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
|
||||||
|
<i class="fas fa-chevron-down"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<select id="status-filter" class="appearance-none bg-gray-50 border border-gray-300 text-gray-700 py-2 px-4 pr-8 rounded-lg leading-tight focus:outline-none focus:bg-white focus:border-primary w-full md:w-auto">
|
||||||
|
<option value="">所有状态</option>
|
||||||
|
<option value="up" {% if current_status == 'up' %}selected{% endif %}>正常</option>
|
||||||
|
<option value="down" {% if current_status == 'down' %}selected{% endif %}>故障</option>
|
||||||
|
<option value="unknown" {% if current_status == 'unknown' %}selected{% endif %}>未知</option>
|
||||||
|
</select>
|
||||||
|
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
|
||||||
|
<i class="fas fa-chevron-down"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<input type="text" id="search-input" placeholder="搜索服务..." value="{{ search_query }}" class="bg-gray-50 border border-gray-300 text-gray-700 py-2 px-4 rounded-lg leading-tight focus:outline-none focus:bg-white focus:border-primary w-full md:w-auto">
|
||||||
|
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||||
|
<i class="fas fa-search text-gray-500"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="search-button" class="bg-primary text-white py-2 px-4 rounded-lg hover:bg-blue-700 transition-colors">
|
||||||
|
<i class="fas fa-filter mr-2"></i>筛选
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 服务表格 -->
|
||||||
|
<div class="bg-white rounded-xl overflow-hidden card-shadow">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">服务名称</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">分组</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">主机/IP</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">最后检测时间</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">响应时间</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200" id="services-tbody">
|
||||||
|
{% for service in services %}
|
||||||
|
<tr class="hover:bg-gray-50">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div class="text-sm font-medium text-gray-900">{{ service.name }}</div>
|
||||||
|
<div class="text-sm text-gray-500">{{ service.check_type }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div class="text-sm text-gray-900">{{ service.group.name }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div class="text-sm text-gray-900">{{ service.host }}</div>
|
||||||
|
<div class="text-sm text-gray-500">{% if service.port %}{{ service.port }}{% else %}N/A{% endif %}</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
{% if service.latest_status == 'UP' %}
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
||||||
|
<i class="fas fa-check-circle mr-1"></i> 正常
|
||||||
|
</span>
|
||||||
|
{% elif service.latest_status == 'DOWN' %}
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
|
||||||
|
<i class="fas fa-times-circle mr-1"></i> 故障
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
|
||||||
|
<i class="fas fa-question-circle mr-1"></i> 未知
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{% if service.latest_check_time %}{{ service.latest_check_time|date:"y-m-d h:i" }}{% else %}从未检测{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{% if service.latest_response_time %}{{ service.latest_response_time }} ms{% else %}N/A{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
|
<a href="/services/{{ service.id }}/" class="text-primary hover:text-blue-700">
|
||||||
|
<i class="fas fa-eye mr-1"></i> 查看详情
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="px-6 py-4 text-center text-gray-500">
|
||||||
|
<i class="fas fa-info-circle text-2xl mb-2"></i>
|
||||||
|
<p>暂无服务数据</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 页脚 -->
|
||||||
|
<footer class="bg-dark text-white py-6 mt-12">
|
||||||
|
<div class="container mx-auto px-4 text-center">
|
||||||
|
<p>© 2025 服务状态监控系统</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 筛选和搜索功能
|
||||||
|
document.getelementbyid('search-button').addeventlistener('click', function() {
|
||||||
|
const groupfilter = document.getelementbyid('group-filter').value;
|
||||||
|
const statusfilter = document.getelementbyid('status-filter').value;
|
||||||
|
const searchquery = document.getelementbyid('search-input').value;
|
||||||
|
|
||||||
|
let url = '/services/?';
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
if (groupfilter) params.push('group=' + encodeuricomponent(groupfilter));
|
||||||
|
if (statusfilter) params.push('status=' + encodeuricomponent(statusfilter));
|
||||||
|
if (searchquery) params.push('search=' + encodeuricomponent(searchquery));
|
||||||
|
|
||||||
|
window.location.href = url + params.join('&');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 回车键触发搜索
|
||||||
|
document.getelementbyid('search-input').addeventlistener('keypress', function(e) {
|
||||||
|
if (e.key === 'enter') {
|
||||||
|
document.getelementbyid('search-button').click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,7 +1,24 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
|
# 引入loguru库用于日志记录
|
||||||
|
try:
|
||||||
|
from loguru import logger
|
||||||
|
except ImportError:
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
# 前端页面路由
|
||||||
path('', views.home, name='home'),
|
path('', views.home, name='home'),
|
||||||
path('api/services/', views.get_services, name='get_services'),
|
path('services/', views.service_list, name='service_list'),
|
||||||
]
|
path('services/<int:service_id>/', views.service_detail, name='service_detail'),
|
||||||
|
|
||||||
|
# API接口路由
|
||||||
|
path('api/checkin/', views.checkin, name='api_checkin'),
|
||||||
|
path('api/services/', views.api_services, name='api_services'),
|
||||||
|
path('api/services/<int:service_id>/history/', views.api_service_history, name='api_service_history'),
|
||||||
|
path('api/status-summary/', views.api_status_summary, name='api_status_summary'),
|
||||||
|
]
|
||||||
|
|
||||||
|
logger.info("URL路由配置已加载")
|
||||||
113
status/utils.py
Normal file
113
status/utils.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
from django.db.models import Count, Case, When, Q, Subquery, OuterRef
|
||||||
|
from .models import ServiceGroup, Service, ServiceCheckRecord
|
||||||
|
|
||||||
|
# 引入loguru库用于日志记录
|
||||||
|
try:
|
||||||
|
from loguru import logger
|
||||||
|
except ImportError:
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 工具函数:获取或创建服务
|
||||||
|
def get_or_create_service(data):
|
||||||
|
"""当客户端上报一个不存在的服务时,自动创建"""
|
||||||
|
service, created = Service.objects.get_or_create(
|
||||||
|
name=data['service_name'],
|
||||||
|
host=data['host'],
|
||||||
|
port=data.get('port'),
|
||||||
|
defaults={
|
||||||
|
'group': ServiceGroup.objects.get_or_create(name="Default")[0],
|
||||||
|
'check_type': data['check_type'],
|
||||||
|
'description': f"Auto-created from client checkin: {data['service_name']}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
logger.info(f"自动创建新服务: {service.name} ({service.host})")
|
||||||
|
return service
|
||||||
|
|
||||||
|
# 工具函数:获取状态摘要
|
||||||
|
def get_status_summary():
|
||||||
|
"""获取全局状态摘要(如:总共服务数、正常数、异常数)"""
|
||||||
|
# 获取每个服务的最新状态
|
||||||
|
latest_records = ServiceCheckRecord.objects.filter(
|
||||||
|
service=OuterRef('pk')
|
||||||
|
).order_by('-checked_at')
|
||||||
|
|
||||||
|
services_with_status = Service.objects.annotate(
|
||||||
|
latest_status=Subquery(latest_records.values('status')[:1])
|
||||||
|
)
|
||||||
|
|
||||||
|
total = services_with_status.count()
|
||||||
|
up_count = services_with_status.filter(latest_status='UP').count()
|
||||||
|
down_count = services_with_status.filter(latest_status='DOWN').count()
|
||||||
|
unknown_count = services_with_status.filter(latest_status='UNKNOWN').count()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total': total,
|
||||||
|
'up': up_count,
|
||||||
|
'down': down_count,
|
||||||
|
'unknown': unknown_count
|
||||||
|
}
|
||||||
|
|
||||||
|
# 工具函数:获取服务的最新状态
|
||||||
|
def get_service_latest_status(service):
|
||||||
|
"""获取服务的最新状态"""
|
||||||
|
latest_record = service.records.order_by('-checked_at').first()
|
||||||
|
if latest_record:
|
||||||
|
return {
|
||||||
|
'status': latest_record.status,
|
||||||
|
'check_time': latest_record.checked_at,
|
||||||
|
'response_time': latest_record.response_time,
|
||||||
|
'message': latest_record.message
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'status': 'UNKNOWN',
|
||||||
|
'check_time': None,
|
||||||
|
'response_time': None,
|
||||||
|
'message': None
|
||||||
|
}
|
||||||
|
|
||||||
|
# 工具函数:获取服务的状态变化时间轴
|
||||||
|
def get_service_status_timeline(service, limit=20):
|
||||||
|
"""获取服务的状态变化时间轴"""
|
||||||
|
status_changes = []
|
||||||
|
prev_status = None
|
||||||
|
|
||||||
|
for record in service.records.all().order_by('-checked_at'):
|
||||||
|
if prev_status is None or record.status != prev_status:
|
||||||
|
status_changes.append(record)
|
||||||
|
prev_status = record.status
|
||||||
|
|
||||||
|
if len(status_changes) >= limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
return status_changes
|
||||||
|
|
||||||
|
# 工具函数:获取服务的响应时间趋势数据
|
||||||
|
def get_service_response_time_chart_data(service, hours=24):
|
||||||
|
"""获取服务的响应时间趋势数据"""
|
||||||
|
from django.utils import timezone
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
end_time = timezone.now()
|
||||||
|
start_time = end_time - datetime.timedelta(hours=hours)
|
||||||
|
|
||||||
|
# 获取指定时间范围内的记录
|
||||||
|
chart_records = service.records.filter(
|
||||||
|
checked_at__gte=start_time,
|
||||||
|
checked_at__lte=end_time,
|
||||||
|
response_time__isnull=False
|
||||||
|
).order_by('checked_at')
|
||||||
|
|
||||||
|
# 准备图表数据
|
||||||
|
chart_labels = []
|
||||||
|
chart_data = []
|
||||||
|
|
||||||
|
for record in chart_records:
|
||||||
|
chart_labels.append(record.checked_at.strftime('%H:%M'))
|
||||||
|
chart_data.append(record.response_time)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'labels': chart_labels,
|
||||||
|
'data': chart_data
|
||||||
|
}
|
||||||
216
status/views.py
216
status/views.py
@@ -1,23 +1,219 @@
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from .models import Service
|
from django.db.models import Count, Case, When, Q, Subquery, OuterRef
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.utils import timezone
|
||||||
|
import json
|
||||||
|
from .models import ServiceGroup, Service, ServiceCheckRecord
|
||||||
|
from .utils import get_or_create_service, get_status_summary, get_service_latest_status, get_service_status_timeline, get_service_response_time_chart_data
|
||||||
|
|
||||||
|
# 引入loguru库用于日志记录
|
||||||
|
try:
|
||||||
|
from loguru import logger
|
||||||
|
except ImportError:
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# 主页视图
|
# 主页视图
|
||||||
def home(request):
|
def home(request):
|
||||||
return render(request, 'status/index.html')
|
"""首页Dashboard"""
|
||||||
|
summary = get_status_summary()
|
||||||
|
# 获取最近异常的服务
|
||||||
|
recent_issues = ServiceCheckRecord.objects.filter(
|
||||||
|
status__in=['DOWN', 'UNKNOWN']
|
||||||
|
).order_by('-checked_at')[:10]
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'summary': summary,
|
||||||
|
'recent_issues': recent_issues
|
||||||
|
}
|
||||||
|
return render(request, 'status/index.html', context)
|
||||||
|
|
||||||
# API视图 - 获取所有服务状态
|
# 服务列表页
|
||||||
def get_services(request):
|
def service_list(request):
|
||||||
|
"""服务列表页"""
|
||||||
|
group_filter = request.GET.get('group')
|
||||||
|
status_filter = request.GET.get('status')
|
||||||
|
search_query = request.GET.get('search', '')
|
||||||
|
|
||||||
services = Service.objects.all()
|
services = Service.objects.all()
|
||||||
|
|
||||||
|
if group_filter:
|
||||||
|
services = services.filter(group__name=group_filter)
|
||||||
|
|
||||||
|
if status_filter:
|
||||||
|
# 根据最新状态筛选
|
||||||
|
latest_records = ServiceCheckRecord.objects.filter(
|
||||||
|
service=OuterRef('pk')
|
||||||
|
).order_by('-checked_at')
|
||||||
|
services = services.annotate(
|
||||||
|
latest_status=Subquery(latest_records.values('status')[:1])
|
||||||
|
).filter(latest_status=status_filter)
|
||||||
|
|
||||||
|
if search_query:
|
||||||
|
services = services.filter(
|
||||||
|
Q(name__icontains=search_query) |
|
||||||
|
Q(host__icontains=search_query) |
|
||||||
|
Q(description__icontains=search_query)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取每个服务的最新状态
|
||||||
|
latest_records = ServiceCheckRecord.objects.filter(
|
||||||
|
service=OuterRef('pk')
|
||||||
|
).order_by('-checked_at')
|
||||||
|
services = services.annotate(
|
||||||
|
latest_status=Subquery(latest_records.values('status')[:1]),
|
||||||
|
latest_check_time=Subquery(latest_records.values('checked_at')[:1]),
|
||||||
|
latest_response_time=Subquery(latest_records.values('response_time')[:1])
|
||||||
|
)
|
||||||
|
|
||||||
|
groups = ServiceGroup.objects.all()
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'services': services,
|
||||||
|
'groups': groups,
|
||||||
|
'current_group': group_filter,
|
||||||
|
'current_status': status_filter,
|
||||||
|
'search_query': search_query
|
||||||
|
}
|
||||||
|
return render(request, 'status/service_list.html', context)
|
||||||
|
|
||||||
|
# 服务详情页
|
||||||
|
def service_detail(request, service_id):
|
||||||
|
"""服务详情页"""
|
||||||
|
service = get_object_or_404(Service, pk=service_id)
|
||||||
|
# 获取最近10条检测记录
|
||||||
|
recent_records = service.records.all()[:10]
|
||||||
|
|
||||||
|
# 获取状态变化时间轴数据
|
||||||
|
status_changes = get_service_status_timeline(service, limit=20)
|
||||||
|
|
||||||
|
# 获取响应时间趋势图数据
|
||||||
|
chart_data = get_service_response_time_chart_data(service, hours=24)
|
||||||
|
|
||||||
|
# 获取服务的最新状态信息
|
||||||
|
latest_status = get_service_latest_status(service)
|
||||||
|
service.latest_status = latest_status['status']
|
||||||
|
service.latest_check_time = latest_status['check_time']
|
||||||
|
service.latest_response_time = latest_status['response_time']
|
||||||
|
service.latest_message = latest_status['message']
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'service': service,
|
||||||
|
'recent_records': recent_records,
|
||||||
|
'status_changes': status_changes,
|
||||||
|
'chart_labels': chart_data['labels'],
|
||||||
|
'chart_data': chart_data['data']
|
||||||
|
}
|
||||||
|
return render(request, 'status/service_detail.html', context)
|
||||||
|
|
||||||
|
# API视图 - 客户端上报接口
|
||||||
|
@csrf_exempt
|
||||||
|
def checkin(request):
|
||||||
|
"""客户端定期调用此接口上报服务状态"""
|
||||||
|
if request.method != 'POST':
|
||||||
|
return JsonResponse({'code': 405, 'message': '只支持POST请求'}, status=405)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
logger.info(f"收到服务状态上报: {data.get('service_name')} - {data.get('status')}")
|
||||||
|
|
||||||
|
# 验证必要字段
|
||||||
|
required_fields = ['service_name', 'host', 'check_type', 'status']
|
||||||
|
for field in required_fields:
|
||||||
|
if field not in data:
|
||||||
|
return JsonResponse({'code': 400, 'message': f'缺少必要字段: {field}'}, status=400)
|
||||||
|
|
||||||
|
# 获取或创建服务
|
||||||
|
service = get_or_create_service(data)
|
||||||
|
|
||||||
|
# 创建检测记录
|
||||||
|
record = ServiceCheckRecord.objects.create(
|
||||||
|
service=service,
|
||||||
|
status=data['status'],
|
||||||
|
response_time=data.get('response_time'),
|
||||||
|
message=data.get('message', '')
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"服务状态已记录: {service.name} - {record.status}")
|
||||||
|
return JsonResponse({
|
||||||
|
'code': 200,
|
||||||
|
'message': '上报成功',
|
||||||
|
'service_id': service.id
|
||||||
|
})
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return JsonResponse({'code': 400, 'message': '无效的JSON数据'}, status=400)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理上报数据时出错: {str(e)}")
|
||||||
|
return JsonResponse({'code': 500, 'message': f'服务器内部错误: {str(e)}'}, status=500)
|
||||||
|
|
||||||
|
# API视图 - 获取所有服务列表(含最新状态)
|
||||||
|
def api_services(request):
|
||||||
|
"""获取所有服务列表(含最新状态)"""
|
||||||
|
# 获取每个服务的最新状态
|
||||||
|
latest_records = ServiceCheckRecord.objects.filter(
|
||||||
|
service=OuterRef('pk')
|
||||||
|
).order_by('-checked_at')
|
||||||
|
|
||||||
|
services = Service.objects.annotate(
|
||||||
|
latest_status=Subquery(latest_records.values('status')[:1]),
|
||||||
|
latest_check_time=Subquery(latest_records.values('checked_at')[:1]),
|
||||||
|
latest_response_time=Subquery(latest_records.values('response_time')[:1])
|
||||||
|
)
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
for service in services:
|
for service in services:
|
||||||
data.append({
|
data.append({
|
||||||
|
'id': service.id,
|
||||||
'name': service.name,
|
'name': service.name,
|
||||||
'status': service.status,
|
'group': service.group.name,
|
||||||
'description': service.description,
|
'host': service.host,
|
||||||
'ip_address': service.ip_address,
|
|
||||||
'port': service.port,
|
'port': service.port,
|
||||||
'reliability': float(service.reliability),
|
'check_type': service.check_type,
|
||||||
'last_updated': service.last_updated.isoformat()
|
'is_active': service.is_active,
|
||||||
|
'latest_status': service.latest_status,
|
||||||
|
'latest_check_time': service.latest_check_time.isoformat() if service.latest_check_time else None,
|
||||||
|
'latest_response_time': service.latest_response_time
|
||||||
})
|
})
|
||||||
|
|
||||||
return JsonResponse(data, safe=False)
|
return JsonResponse(data, safe=False)
|
||||||
|
|
||||||
|
# API视图 - 获取某服务历史记录(分页)
|
||||||
|
def api_service_history(request, service_id):
|
||||||
|
"""获取某服务历史记录(分页)"""
|
||||||
|
service = get_object_or_404(Service, pk=service_id)
|
||||||
|
|
||||||
|
# 分页参数
|
||||||
|
page = int(request.GET.get('page', 1))
|
||||||
|
page_size = int(request.GET.get('page_size', 20))
|
||||||
|
|
||||||
|
# 计算分页范围
|
||||||
|
start = (page - 1) * page_size
|
||||||
|
end = start + page_size
|
||||||
|
|
||||||
|
records = service.records.all()[start:end]
|
||||||
|
total = service.records.count()
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for record in records:
|
||||||
|
data.append({
|
||||||
|
'id': record.id,
|
||||||
|
'status': record.status,
|
||||||
|
'response_time': record.response_time,
|
||||||
|
'message': record.message,
|
||||||
|
'checked_at': record.checked_at.isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'total': total,
|
||||||
|
'page': page,
|
||||||
|
'page_size': page_size,
|
||||||
|
'records': data
|
||||||
|
})
|
||||||
|
|
||||||
|
# API视图 - 获取全局状态摘要
|
||||||
|
def api_status_summary(request):
|
||||||
|
"""获取全局状态摘要(如:总共服务数、正常数、异常数)"""
|
||||||
|
summary = get_status_summary()
|
||||||
|
return JsonResponse(summary)
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user