使用Django,Prometheus,和Kubernetes定制应用指标
编者按
本文强调了应用程序定制指标的重要性,用代码实例演示了如何设计指标并整合Prometheus到Django项目中,为使用Django构建应用的开发者提供了参考。
为什么自定义指标很重要?
尽管有大量关于这一主题的讨论,但应用程序的自定义指标的重要性怎么强调都不为过。和为Django应用收集的核心服务指标(应用和web服务器统计数据、关键数据库和缓存操作指标)不同,自定义指标是业务特有的数据点,其边界和阈值只有你自己知道,这其实是很有趣的事情。
什么样的指标才是有用的?考虑下面几点:
- 运行一个电子商务网站并追踪平均订单数量。突然间订单的数量不那么平均了。有了可靠的应用指标和监控,你就可以在损失殆尽之前捕获到Bug。
- 你正在写一个爬虫,它每小时从一个新闻网站抓取最新的文章。突然最近的文章并不新了。可靠的指标和监控可以更早地揭示问题所在。
- 我认为你已经理解了重点。
设置Django应用程序
除了明显的依赖(pip install Django
)之外,我们还需要为宠物项目(译者注:demo)添加一些额外的包。继续并安装pip install django-prometheus-client
。这将为我们提供一个Python的Prometheus客户端,以及一些有用的Django hook,包括中间件和一个优雅的DB包装器。接下来,我们将运行Django管理命令来启动项目,更新我们的设置来使用Prometheus客户端,并将Prometheus的URL添加到URL配置中。
启动一个新的项目和应用程序
为了这篇文章,并且切合代理的品牌,我们建立了一个遛狗服务。请注意,它实际上不会做什么事,但足以作为一个教学示例。执行如下命令:
django-admin.py startproject demo
python manage.py startapp walker
#settings.py
INSTALLED_APPS = [
...
'walker',
...
]
现在,我们来添加一些基本的模型和视图。简单起见,我只实现将要验证的部分。如果想要完整地示例,可以从这个demo应用 获取源码。
# walker/models.py
from django.db import models
from django_prometheus.models import ExportModelOperationsMixin
class Walker(ExportModelOperationsMixin('walker'), models.Model):
name = models.CharField(max_length=127)
email = models.CharField(max_length=127)
def __str__(self):
return f'{self.name} // {self.email} ({self.id})'
class Dog(ExportModelOperationsMixin('dog'), models.Model):
SIZE_XS = 'xs'
SIZE_SM = 'sm'
SIZE_MD = 'md'
SIZE_LG = 'lg'
SIZE_XL = 'xl'
DOG_SIZES = (
(SIZE_XS, 'xsmall'),
(SIZE_SM, 'small'),
(SIZE_MD, 'medium'),
(SIZE_LG, 'large'),
(SIZE_XL, 'xlarge'),
)
size = models.CharField(max_length=31, choices=DOG_SIZES, default=SIZE_MD)
name = models.CharField(max_length=127)
age = models.IntegerField()
def __str__(self):
return f'{self.name} // {self.age}y ({self.size})'
class Walk(ExportModelOperationsMixin('walk'), models.Model):
dog = models.ForeignKey(Dog, related_name='walks', on_delete=models.CASCADE)
walker = models.ForeignKey(Walker, related_name='walks', on_delete=models.CASCADE)
distance = models.IntegerField(default=0, help_text='walk distance (in meters)')
start_time = models.DateTimeField(null=True, blank=True, default=None)
end_time = models.DateTimeField(null=True, blank=True, default=None)
@property
def is_complete(self):
return self.end_time is not None
@classmethod
def in_progress(cls):
""" get the list of `Walk`s currently in progress """
return cls.objects.filter(start_time__isnull=False, end_time__isnull=True)
def __str__(self):
return f'{self.walker.name} // {self.dog.name} @ {self.start_time} ({self.id})'
# walker/views.py
from django.shortcuts import render, redirect
from django.views import View
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponseNotFound, JsonResponse, HttpResponseBadRequest, Http404
from django.urls import reverse
from django.utils.timezone import now
from walker import models, forms
class WalkDetailsView(View):
def get_walk(self, walk_id=None):
try:
return models.Walk.objects.get(id=walk_id)
except ObjectDoesNotExist:
raise Http404(f'no walk with ID {walk_id} in progress')
class CheckWalkStatusView(WalkDetailsView):
def get(self, request, walk_id=None, **kwargs):
walk = self.get_walk(walk_id=walk_id)
return JsonResponse({'complete': walk.is_complete})
class CompleteWalkView(WalkDetailsView):
def get(self, request, walk_id=None, **kwargs):
walk = self.get_walk(walk_id=walk_id)
return render(request, 'index.html', context={'form': forms.CompleteWalkForm(instance=walk)})
def post(self, request, walk_id=None, **kwargs):
try:
walk = models.Walk.objects.get(id=walk_id)
except ObjectDoesNotExist:
return HttpResponseNotFound(content=f'no walk with ID {walk_id} found')
if walk.is_complete:
return HttpResponseBadRequest(content=f'walk {walk.id} is already complete')
form = forms.CompleteWalkForm(data=request.POST, instance=walk)
if form.is_valid():
updated_walk = form.save(commit=False)
updated_walk.end_time = now()
updated_walk.save()
return redirect(f'{reverse("walk_start")}?walk={walk.id}')
return HttpResponseBadRequest(content=f'form validation failed with errors {form.errors}')
class StartWalkView(View):
def get(self, request):
return render(request, 'index.html', context={'form': forms.StartWalkForm()})
def post(self, request):
form = forms.StartWalkForm(data=request.POST)
if form.is_valid():
walk = form.save(commit=False)
walk.start_time = now()
walk.save()
return redirect(f