From 1cac84b9d47f8ea809e45c190acbce71db3ed4cc Mon Sep 17 00:00:00 2001 From: xiaji Date: Sun, 7 Sep 2025 16:47:12 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=96=B0=E6=9E=B6=E6=9E=84=E4=BA=86?= =?UTF-8?q?=E6=95=B4=E4=B8=AAmodels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db.sqlite3 | Bin 135168 -> 155648 bytes requirements.txt | 4 +- status/__pycache__/admin.cpython-38.pyc | Bin 620 -> 2371 bytes status/__pycache__/models.cpython-38.pyc | Bin 1400 -> 3483 bytes status/__pycache__/urls.cpython-38.pyc | Bin 353 -> 839 bytes status/__pycache__/views.cpython-38.pyc | Bin 841 -> 5401 bytes status/admin.py | 51 ++- status/migrations/0001_initial.py | 54 ++- .../__pycache__/0001_initial.cpython-38.pyc | Bin 1402 -> 2306 bytes status/models.py | 75 +++- status/templates/status/index.html | 139 ++++--- status/templates/status/service_detail.html | 340 ++++++++++++++++++ status/templates/status/service_list.html | 216 +++++++++++ status/urls.py | 21 +- status/utils.py | 113 ++++++ status/views.py | 216 ++++++++++- .../__pycache__/settings.cpython-38.pyc | Bin 2443 -> 2497 bytes 17 files changed, 1141 insertions(+), 88 deletions(-) create mode 100644 status/templates/status/service_detail.html create mode 100644 status/templates/status/service_list.html create mode 100644 status/utils.py diff --git a/db.sqlite3 b/db.sqlite3 index 8828eb8cf42a69fbda04c4242a9e57192013a9d8..70bf373489051523d998d6a75f4248efd64db378 100644 GIT binary patch delta 3298 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 diff --git a/requirements.txt b/requirements.txt index c023ba7..791dd34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ -Django==4.2.7 \ No newline at end of file +Django==4.2.7 +djangorestframework==3.14.0 +loguru==0.7.2 \ No newline at end of file diff --git a/status/__pycache__/admin.cpython-38.pyc b/status/__pycache__/admin.cpython-38.pyc index f5334ac8e3e3a747dbe17e7a1059dc1490d2d6c3..83f5a0299d48d30216d2372ba1bb3cb7d5ceeb26 100644 GIT binary patch literal 2371 zcmaKt&2Jnv6u|8n&(40PX%uLIP@;lLr_dmAj1UScP$88ZDCJ|c(r7w%b|;KHV329NEQo=26Csxb$?H2P{%kdpb7Dr(OOA0O5 zcPaUlw2v_&iWkcA6H4$0t&^1ek|?c~MP-fp?&qX!eMSUbtqOaMo+iA&-DSdELFa3i z?aS03F*;^+MP343X;*i^=p3Uf^9txHySh=~t{&voHMG<@OT5}P*iSsFoshTU_BO?q zt*d8+yc9>`v`o7l?G9RcY*0DX6w!r`M3hPnQwvGjXb3qQ;Zpg}9yHV|P!YtZocI=} zz72(4C36%nYq{t{6c1GmRapOx>+?8{`8Ey1`Ydx@8doIqgbuu`5)j?ezS-W-z0r3sEy{H^e&Cr67osP^lnTCx4xRrA|%P&nY2Q{fju9 zkL&NIQMV=9nW{he?e3FDV9eDgk018#{9fG{i-aq)t*#AD7KtPY0{JW$&=+{+60cXHRTU@--CDF5WtQpt zZ^Vwnz<(iT`Hb6$7GTD>1DF*IW-hq5unTyfFn8dqhQXaRQ*a9Fq%zax2vHBRg^tjr zSOsB}#g_zhOA%&*2Vv&f@+H_?9!2po25cNmcKh zC71E~$T00fFk+6>g?@19niQ&&wv`C77}8ZAogGFzu)rS~ObG%*nnY+l%ZDH_EA^vG zyDgzq2pRP?R6Ayr#QE|Biq}z~DzZTAd|1cieF}3O|o+)A(DIT zdImS>r^B3SZ@|bM(@y@MX+twTI_s79BTyd0-H|*!!Xc`IW-KITtp$Am!knk=WML

5mI0cOTbWg~CF;5e{MNm-ty@=mx4!A!yubbRjjfG)ywwh*`!tzLB`5{4tScDch9GbZ}pS@+vrz? ZAzY>7wnazj2rbf4t4PNyP^wUB{{T!yS&IMw delta 439 zcmYk2!Ab)$5QdX(vb(n1OZ8w8JO~O)U!h1nd9nz_iyoG^8Equ(E=el%t`GnsG5eD!|W;Hukg6SVU9a(Y7v`3Yj%7>W_P9lj0vf|7G0 zndC1-a-a|FIl2xxeor6*{?VdD#uJh!tANO<=R(fZe1+hkk_nhcRYDo6+ZO60bi3bt z^ZpYOxX(6^&jKM&Je!Ld_}Dt(7S?wp*wU!lscP=qsxE|-2CVh0u8i{?16qk&rIlOy zObhFZMJ?c|2JUePn19>`yob#` T9nLDb&~O@-vqon$YxVdaS8Zx$ diff --git a/status/__pycache__/models.cpython-38.pyc b/status/__pycache__/models.cpython-38.pyc index 48a79f43c9b96dcc1ab058ad3a765ad27e51d8a5..54831d7e40592dad8c941d07a0569b9376253918 100644 GIT binary patch literal 3483 zcmbtX-ES1v6`!x&nfcE#{e}kvLqO$%lK-mkIe-Q+ci0nvp*_35`Rve|SnyM^K zbc3ckT0LPVWa%}@-JwY|c|6TZvV|qE?}Ym=`u$1coi&&w^~uPDK~zqw{aAE>9L!p03*`Er+_*V2me2xAuBSdG-Wl zM?Iffu3e}1lh|o`dhOn=1(hd`I<|Y9Yi{T`45z3fgXkRijMo5MKK45;jZ5H-T!Pp& z9Bl#aK6Q>QNUR6m-#>z__l%DCl=-74Yxa0;bkM7Wb?OHG=*r!jD@#kwU!Pl9z7u`$ z`DpaPbnE7aqpKJ1G|#-*n*Xdd_uElFu!GPajkDd~IK_3#@&jgBUFd@XLXs!?*5{(X z-C)BUpyzwx^7}w^=77ki3YSI{h(n#&^vFm@XkarPT9+0Q+^|HxEQ_Zt3;YQk3eQ=~ zdOfrqjNP(C#(3}g^jZxkWVR!+u}<{Dpj^Z8F-^V(!jLtYZNrZh(`*ae*;W)!p};tK zYGBM}2W!+J41?#XGCdgxoo5L3E3DQCYM#q5JTcI^@8MS%ev}~+GjRD>Vusu;cPoY} ztQ!)Bk?+N(e+2O`VF=hN6d={c6GRhWRm>!4IyFG15Vj;WrI~5bB26*?XT{8bmL+)R zNFLyw6K}gnH`zpbNH6KDYGgCn0{!yvrc1QkN}ht2-Jov*{b{ld#_0jA7qn-{cF_9B zv*fu+$=nP@@-3p8+?ddEkTp1Y(1=K&e5#7BD25s=w*GjSJonN%B z&VorFDZNs9V4c0W`u>Nlzd-Eay283Vwfga`qQWqJV!llSz>SsY$IULBiEdweP=6`U zwSXvS0ATZ}f8*dO5G!|=o3l&14-b_?oT}B|FGkZhzWU;PG`-k5bK$Em zE-a`lk6+B8z`|_32!d;knp*{Lc7hV3c?k68jWf;9-YzPp8dMq~Vx>GV6c5>X#kH{@ zXowy!m&-$Ii+^t3{yFqWRzg4U>Y~%yxtr0aS64p2ur_(MIs1n=Ix+O>?d9m&+eJ+z zf$M}GvqLA~dS%SRJP7|7f{bC}L`2Q^7Ng4_MVA*>ul#Ox=9kS&A1^5ETi67>7Gp=} zXQJh$=DT-V@BRrUBWs6&XSv=RmQ9FgwSkMUYy=k}PWS~+e7ry7)P$8n#<9@+h#=TO^? zTc&EhWmj+jo*M*omtip!Z=g4$0`7M3KPc%r7#0c7M?4scBE$JFEt!}5kUuB~7(h)NScS3qH4NrMH53QnLe z(6gZDgbw=v(DR^o(QY8349Ox50pS3V0pa9&kD|78j#S??*G&<5CnvCTckD%yvPa4LN+U zQX&>RIV5rw*GdJ~N;5%Jh)bK;x-l*R^@+?20zANWuzn-B<9$iYKWm@fioSfOR7^e& zX#?^?8Bmyrm0>{Z%Jk71`98c9n-D`zc9zZ!41s7641xGXh7jC<9HAgbz+Ih@<`OYe zsD&2_6I#Iz3Pp|UZB~zqDH-oWcw7=9s{cwNFQH@YoyoQ*T% z2O^NJ%Yw@k^iX)J!CeK96L8n&^m*6~2g$S2xPDqbty~g&V~8VB3USs}?#)G?ytgsw zdkTW|cNJoMfr@zARalRxJq4}+s0@Y-#I>4x%ws=D^*}VD2R^3LkbB^Strcvp@(Exnd|S-(6dAYOs@G4B#yh%f zTMvVp;|m{5{DTNt7^gmSxQ;}8fH9A89sez$GDm4t<6(n5 literal 1400 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 O^A?pQSDUw)xBnL-4B8?9 literal 841 zcmY*XO=}cE5bd6sotgcLiJtTy>?K}2h=>RY9z=o|a~VcLXS3CjFNz@?{`HTmxBOW}U_a|7Hi=`dZ~u z=?=H8`%vVM6qH2{YQpgtox!rwTS-@SX@5S*b`c8*Ahm`KQOK@?ft{sib6|uS!1V+J zZ_oEkmTD4B(X`qeC@=?1hv@nF0BPY?yaGx?yq$=C5-%3rL8fv8@yVCNljGyFcdt&q z9i8rfjZgRAP7XiCKVBc5J%2G7f1Z5!7*B7Cw-WUf=F1)H^@>dS7!~M7#x<$?PrHYX zCZf{*7!AYve9sxP5#L}VXUG@XlN%#<=>~iGhUTch5zYoS( z{LRazJ#?fa-8WPPZ>bYlFBh4z{#2uFN(Eh=lqM;1+epe)A*EIT)+-BbY*VW=5o<}B zm>t_n1(?-ADTPt;9v036v93BZno_etvDQ~z1J)I3YJE|b*g{Q7=@wC&Xolz#lC7<$ zg)n-S9CPHTCs3>xIzuN_qvPoSmng(8P^(c9-YcTu@4jl)l diff --git a/status/admin.py b/status/admin.py index 715c2ff..00ac340 100644 --- a/status/admin.py +++ b/status/admin.py @@ -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("管理后台配置已加载") diff --git a/status/migrations/0001_initial.py b/status/migrations/0001_initial.py index 5f01ade..707dd86 100644 --- a/status/migrations/0001_initial.py +++ b/status/migrations/0001_initial.py @@ -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='所属分组'), + ), ] diff --git a/status/migrations/__pycache__/0001_initial.cpython-38.pyc b/status/migrations/__pycache__/0001_initial.cpython-38.pyc index c7dc611335e686271c4e1d9399c18bf79dbea156..852248cebd705f7cce4272af8668da85f2f3e934 100644 GIT binary patch literal 2306 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 R3ZTpMTC0cJs*A8*?jPtd_Co*w diff --git a/status/models.py b/status/models.py index bb91ffe..fef97af 100644 --- a/status/models.py +++ b/status/models.py @@ -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) diff --git a/status/templates/status/index.html b/status/templates/status/index.html index fe7163a..f3736ee 100644 --- a/status/templates/status/index.html +++ b/status/templates/status/index.html @@ -5,7 +5,7 @@ 服务状态监控 - + + + + + + + + +

+ +
+ + +
+ +
+
+
+

{{ service.name }}

+
+ {% if service.latest_status == 'UP' %} + + 正常 + + {% elif service.latest_status == 'DOWN' %} + + 故障 + + {% else %} + + 未知 + + {% endif %} + + + {% if service.latest_check_time %}最后检测: {{ service.latest_check_time|date:"y-m-d h:i:s" }}{% else %}从未检测{% endif %} + +
+
+ + 返回服务列表 + +
+ +
+
+

基本信息

+
+
+ 服务名称: + {{ service.name }} +
+
+ 所属分组: + {{ service.group.name }} +
+
+ 主机/IP: + {{ service.host }} +
+
+ 端口: + {% if service.port %}{{ service.port }}{% else %}N/A{% endif %} +
+
+ 检测类型: + {{ service.get_check_type_display }} +
+
+ 是否启用: + {% if service.is_active %}是{% else %}否{% endif %} +
+
+
+ +
+

最新状态

+
+
+ 当前状态: + + {% if service.latest_status == 'UP' %}正常 + {% elif service.latest_status == 'DOWN' %}故障 + {% else %}未知{% endif %} + +
+
+ 响应时间: + {% if service.latest_response_time %}{{ service.latest_response_time }} ms{% else %}N/A{% endif %} +
+
+ 最后检测: + {% if service.latest_check_time %}{{ service.latest_check_time|date:"y-m-d h:i:s" }}{% else %}从未检测{% endif %} +
+
+ 检测消息: + {% if service.latest_message %}{{ service.latest_message }}{% else %}无{% endif %} +
+
+
+
+ + {% if service.description %} +
+

描述

+

{{ service.description }}

+
+ {% endif %} +
+ + +
+

响应时间趋势 (最近24小时)

+
+ +
+
+ + +
+

状态变化时间轴

+
+
+
+ {% for record in status_changes %} +
+
+ {% if record.status == 'UP' %} + {% elif record.status == 'DOWN' %} + {% else %}{% endif %} +
+
+
+ + {% if record.status == 'UP' %}正常 + {% elif record.status == 'DOWN' %}故障 + {% else %}未知{% endif %} + + {{ record.checked_at|date:"y-m-d h:i:s" }} +
+ {% if record.message %} +

{{ record.message }}

+ {% endif %} + {% if record.response_time %} +

响应时间: {{ record.response_time }} ms

+ {% endif %} +
+
+ {% empty %} +
+ +

暂无状态变化记录

+
+ {% endfor %} +
+
+
+ + +
+
+

最近检测记录

+
+
+ + + + + + + + + + + {% for record in recent_records %} + + + + + + + {% empty %} + + + + {% endfor %} + +
检测时间状态响应时间消息
{{ record.checked_at|date:"y-m-d h:i:s" }} + {% if record.status == 'UP' %} + + 正常 + + {% elif record.status == 'DOWN' %} + + 故障 + + {% else %} + + 未知 + + {% endif %} + + {% if record.response_time %}{{ record.response_time }} ms{% else %}N/A{% endif %} + + {% if record.message %}{{ record.message }}{% else %}无{% endif %} +
+ +

暂无检测记录

+
+
+
+
+ + +
+
+

© 2025 服务状态监控系统

+
+
+ + + + \ No newline at end of file diff --git a/status/templates/status/service_list.html b/status/templates/status/service_list.html new file mode 100644 index 0000000..e2ba02e --- /dev/null +++ b/status/templates/status/service_list.html @@ -0,0 +1,216 @@ + + + + + + 服务列表 - 服务状态监控 + + + + + + + +
+ +
+ + +
+ +
+
+

服务列表

+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+
+ + +
+
+ + + + + + + + + + + + + + {% for service in services %} + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
服务名称分组主机/IP状态最后检测时间响应时间操作
+
{{ service.name }}
+
{{ service.check_type }}
+
+
{{ service.group.name }}
+
+
{{ service.host }}
+
{% if service.port %}{{ service.port }}{% else %}N/A{% endif %}
+
+ {% if service.latest_status == 'UP' %} + + 正常 + + {% elif service.latest_status == 'DOWN' %} + + 故障 + + {% else %} + + 未知 + + {% endif %} + + {% if service.latest_check_time %}{{ service.latest_check_time|date:"y-m-d h:i" }}{% else %}从未检测{% endif %} + + {% if service.latest_response_time %}{{ service.latest_response_time }} ms{% else %}N/A{% endif %} + + + 查看详情 + +
+ +

暂无服务数据

+
+
+
+
+ + +
+
+

© 2025 服务状态监控系统

+
+
+ + + + \ No newline at end of file diff --git a/status/urls.py b/status/urls.py index 9bbb9fa..78abbca 100644 --- a/status/urls.py +++ b/status/urls.py @@ -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'), -] \ No newline at end of file + path('services/', views.service_list, name='service_list'), + path('services//', 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//history/', views.api_service_history, name='api_service_history'), + path('api/status-summary/', views.api_status_summary, name='api_status_summary'), +] + +logger.info("URL路由配置已加载") \ No newline at end of file diff --git a/status/utils.py b/status/utils.py new file mode 100644 index 0000000..b69f95a --- /dev/null +++ b/status/utils.py @@ -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 + } \ No newline at end of file diff --git a/status/views.py b/status/views.py index 554bbcc..f34cf84 100644 --- a/status/views.py +++ b/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) diff --git a/statuspage/__pycache__/settings.cpython-38.pyc b/statuspage/__pycache__/settings.cpython-38.pyc index 989515d76be774c29a3f29dcd620e2975d3144d3..cebdec334184cf0998429e2a09387e236c3a814e 100644 GIT binary patch delta 657 zcmZ`#%Wl&^6!qBSyqdUal0IllX=w`ts0(}nrk#SJbs{?v(ts4DI)ib^1C5I!vdMxS zED-883;GRRvtYp=@DcCd^8TVJ-SD^37sck@MNg*6evt2kVFb;WH1({cp6#cU|}42 zOrU_G!ZIlFF-(RU3`}7fGq`}+kVXV^LmCpE#XQg9A{K;+Czs|lEDrUEa0^SgtVl~( z9>tYWT#ZOb`8cldJg)Ky^_7BpMO@=0l=&pCYg)EzV zW|M6>_3c`t>fCodr{-3?#>Tc21U#(})(BkA_`+Y3Q?ET>Ud`S3hsXjxJL=#0t-|~r zHwaEuRP%I$&%90MGyg=N1zvgj=zTIrl$$;aYK<-5*{-c9oDqei6yvZ8*BsHi` b6FNC_f70y1dirUB(h;1{k9AXJ$NG<7UI@7O delta 602 zcma))&2AGh6ou`HlSw8sY1*_Y&;l;dhVU~ZRh5ufBiNxz6&njFn&vnYNAnYn9e!3> zuw;d@?E=Ij^aaXm@Bm0W0ul>M+KMi*eXnHcp7R|`{?_@}X}t;pkJxqCJshjxQ)_#E zW^W*ZfWk>><_L!p>B!0l`k8$plj@`@YN!YoOwL+oFY=xFzQ`+}