重新架构了整个models
This commit is contained in:
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 .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):
|
||||
list_display = ('name', 'status', 'description', 'ip_address', 'port', 'reliability', 'last_updated')
|
||||
list_filter = ('status',)
|
||||
search_fields = ('name', 'ip_address', 'description')
|
||||
list_display = ('name', 'group', 'host', 'port', 'check_type', 'is_active', 'created_at')
|
||||
list_filter = ('group', 'check_type', 'is_active')
|
||||
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
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
@@ -16,17 +17,50 @@ class Migration(migrations.Migration):
|
||||
name='Service',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='服务名称')),
|
||||
('status', models.CharField(choices=[('operational', '正常运行'), ('degraded', '性能下降'), ('outage', '服务中断')], default='operational', max_length=20, verbose_name='服务状态')),
|
||||
('description', models.TextField(verbose_name='服务描述')),
|
||||
('ip_address', models.GenericIPAddressField(verbose_name='IP地址')),
|
||||
('port', models.PositiveIntegerField(verbose_name='端口号')),
|
||||
('reliability', models.DecimalField(decimal_places=2, default=99.0, max_digits=5, verbose_name='可靠率(%)')),
|
||||
('last_updated', models.DateTimeField(default=django.utils.timezone.now, verbose_name='最后更新时间')),
|
||||
('name', models.CharField(max_length=200, verbose_name='服务名称')),
|
||||
('host', models.CharField(max_length=255, verbose_name='主机/IP')),
|
||||
('port', models.PositiveIntegerField(blank=True, null=True, verbose_name='端口(可选)')),
|
||||
('check_type', models.CharField(choices=[('ping', 'Ping检测'), ('tcp', 'TCP端口检测'), ('http', 'HTTP请求'), ('custom', '自定义脚本')], default='ping', max_length=50, 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='创建时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '服务状态',
|
||||
'verbose_name_plural': '服务状态',
|
||||
'verbose_name': '服务',
|
||||
'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.utils import timezone
|
||||
|
||||
class Service(models.Model):
|
||||
STATUS_CHOICES = (
|
||||
('operational', '正常运行'),
|
||||
('degraded', '性能下降'),
|
||||
('outage', '服务中断'),
|
||||
)
|
||||
|
||||
name = models.CharField(max_length=100, verbose_name='服务名称')
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='operational', verbose_name='服务状态')
|
||||
description = models.TextField(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='最后更新时间')
|
||||
# 引入loguru库用于日志记录
|
||||
try:
|
||||
from loguru import logger
|
||||
except ImportError:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ServiceGroup(models.Model):
|
||||
name = models.CharField(max_length=100, unique=True, verbose_name="服务分组")
|
||||
description = models.TextField(blank=True, null=True, verbose_name="描述")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = '服务状态'
|
||||
verbose_name_plural = '服务状态'
|
||||
verbose_name = '服务分组'
|
||||
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">
|
||||
<title>服务状态监控</title>
|
||||
<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>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
@@ -52,13 +52,18 @@
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<i class="fa fa-refresh animate-spin mr-2"></i>
|
||||
<span id="last-updated" class="text-sm md:text-base">最后更新: 加载中...</span>
|
||||
</div>
|
||||
<i class="fas fa-refresh animate-spin mr-2"></i>
|
||||
<span id="last-updated" class="text-sm md:text-base">最后更新: 加载中...</span>
|
||||
</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>
|
||||
<p class="mt-2 text-gray-200">实时监控系统服务运行状态与可靠性</p>
|
||||
</div>
|
||||
@@ -71,43 +76,72 @@
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<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 class="bg-white rounded-xl p-6 card-shadow card-hover border-l-4 border-degraded">
|
||||
<div class="flex items-center justify-between">
|
||||
<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>
|
||||
</div>
|
||||
<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 class="bg-white rounded-xl p-6 card-shadow card-hover border-l-4 border-outage">
|
||||
<div class="flex items-center justify-between">
|
||||
<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>
|
||||
</div>
|
||||
<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 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">
|
||||
<!-- 服务卡片将通过JavaScript动态生成 -->
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -123,20 +157,20 @@
|
||||
<script>
|
||||
// 状态样式映射
|
||||
const statusStyles = {
|
||||
operational: {
|
||||
up: {
|
||||
class: 'bg-operational text-white',
|
||||
icon: 'fa-check-circle',
|
||||
text: '正常运行'
|
||||
icon: 'fas fa-check-circle',
|
||||
text: '正常'
|
||||
},
|
||||
degraded: {
|
||||
class: 'bg-degraded text-white',
|
||||
icon: 'fa-exclamation-triangle',
|
||||
text: '性能下降'
|
||||
},
|
||||
outage: {
|
||||
down: {
|
||||
class: 'bg-outage text-white',
|
||||
icon: 'fa-times-circle',
|
||||
text: '服务中断'
|
||||
icon: 'fas fa-times-circle',
|
||||
text: '故障'
|
||||
},
|
||||
unknown: {
|
||||
class: 'bg-degraded text-white',
|
||||
icon: 'fas fa-question-circle',
|
||||
text: '未知'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -151,19 +185,23 @@
|
||||
|
||||
// 统计各状态数量
|
||||
const counts = {
|
||||
operational: 0,
|
||||
degraded: 0,
|
||||
outage: 0
|
||||
up: 0,
|
||||
down: 0,
|
||||
unknown: 0
|
||||
};
|
||||
|
||||
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('degraded-count').textContent = counts.degraded;
|
||||
document.getElementById('outage-count').textContent = counts.outage;
|
||||
document.getElementById('operational-count').textContent = counts.up;
|
||||
document.getElementById('degraded-count').textContent = counts.unknown;
|
||||
document.getElementById('outage-count').textContent = counts.down;
|
||||
|
||||
// 生成服务卡片
|
||||
const container = document.getElementById('services-container');
|
||||
@@ -172,7 +210,7 @@
|
||||
if (data.length === 0) {
|
||||
container.innerHTML = `
|
||||
<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>
|
||||
</div>
|
||||
`;
|
||||
@@ -180,7 +218,8 @@
|
||||
}
|
||||
|
||||
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');
|
||||
card.className = 'bg-white rounded-xl overflow-hidden card-shadow card-hover';
|
||||
card.innerHTML = `
|
||||
@@ -188,29 +227,41 @@
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<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}">
|
||||
<i class="fa ${style.icon} mr-1"></i> ${style.text}
|
||||
<i class="fas ${style.icon} mr-1"></i> ${style.text}
|
||||
</span>
|
||||
</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="bg-gray-50 p-3 rounded-lg">
|
||||
<p class="text-gray-500">IP地址</p>
|
||||
<p class="font-medium">${service.ip_address}</p>
|
||||
<p class="text-gray-500">主机/IP</p>
|
||||
<p class="font-medium">${service.host}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-3 rounded-lg">
|
||||
<p class="text-gray-500">端口</p>
|
||||
<p class="font-medium">${service.port}</p>
|
||||
<p class="font-medium">${service.port || 'N/A'}</p>
|
||||
</div>
|
||||
${service.latest_response_time ? `
|
||||
<div class="bg-gray-50 p-3 rounded-lg col-span-2">
|
||||
<p class="text-gray-500">可靠率</p>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2 mt-1">
|
||||
<div class="${style.class} h-2 rounded-full" style="width: ${service.reliability}%"></div>
|
||||
</div>
|
||||
<p class="font-medium mt-1">${service.reliability}%</p>
|
||||
<p class="text-gray-500">响应时间</p>
|
||||
<p class="font-medium">${service.latest_response_time} ms</p>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<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>
|
||||
`;
|
||||
@@ -222,7 +273,7 @@
|
||||
const container = document.getElementById('services-container');
|
||||
container.innerHTML = `
|
||||
<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>
|
||||
</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 . import views
|
||||
|
||||
# 引入loguru库用于日志记录
|
||||
try:
|
||||
from loguru import logger
|
||||
except ImportError:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
urlpatterns = [
|
||||
# 前端页面路由
|
||||
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 .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):
|
||||
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()
|
||||
|
||||
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 = []
|
||||
for service in services:
|
||||
data.append({
|
||||
'id': service.id,
|
||||
'name': service.name,
|
||||
'status': service.status,
|
||||
'description': service.description,
|
||||
'ip_address': service.ip_address,
|
||||
'group': service.group.name,
|
||||
'host': service.host,
|
||||
'port': service.port,
|
||||
'reliability': float(service.reliability),
|
||||
'last_updated': service.last_updated.isoformat()
|
||||
'check_type': service.check_type,
|
||||
'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)
|
||||
|
||||
# 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)
|
||||
|
||||
Reference in New Issue
Block a user