重新架构了整个models

This commit is contained in:
2025-09-07 16:47:12 +08:00
parent 1a4271232f
commit 1cac84b9d4
17 changed files with 1141 additions and 88 deletions

Binary file not shown.

View File

@@ -1 +1,3 @@
Django==4.2.7 Django==4.2.7
djangorestframework==3.14.0
loguru==0.7.2

View File

@@ -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("管理后台配置已加载")

View File

@@ -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='所属分组'),
),
] ]

View File

@@ -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)

View File

@@ -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>
`; `;

View 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>

View 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>

View File

@@ -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
View 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
}

View File

@@ -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)