編者按
本文強調了應用程序定制指標的重要性,用代碼實例演示了如何設計指標並整合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'{reverse("walk_start")}?walk={walk.id}') return HttpResponseBadRequest