背景
Prometheus是最近流行的監控報警系統,具體大家可以搜網上的文章來了解,而由於我司目前的應用使用了Django框架來做爲後端應用,因此需要研究如何將Prometheus與Django結合在一起使用,因此有了接下來的源碼研究。
在分析源代碼之前,先要知道爲什麼需要分析源代碼,對於我來說,有幾個問題是我想要搞明白的:
-
django-prometheus
是如何註冊/metrics
uri並通過接口提供服務的? -
django-prometheus
到底是怎樣將數據從不同的接口收集上來的? -
django-prometheus
收集上來Metrics後是否需要存儲,如果需要,那麼存儲在什麼地方了?
而在搞清楚這些問題的時候,發現django-prometheus
又調用了prometheus_client
,又不可避免的有了針對prometheus_client
的問題,所以又不得不去看prometheus_client
的源碼,也因此有了本文。
接下來就分別從這三個問題出發,看下django-prometheus
的內部實現究竟是怎麼樣的?
源碼分析
django-prometheus
註冊/metrics
URL
首先在使用django-prometheus
的時候需要如下注冊URL
urlpatterns = [
...
url('', include('django_prometheus.urls')),
]
因此我們找到這個urls文件,看下究竟是啥
# django_prometheus/urls.py
from django.urls import path
from django_prometheus import exports
urlpatterns = [
path("metrics", exports.ExportToDjangoView, name="prometheus-django-metrics")
]
看到這裏註冊了一個/metrics
API,而實際的view函數是這個exports.ExportToDjangoView
,再找到這個函數,如下:
# django_prometheus/exports.py
def ExportToDjangoView(request):
"""Exports /metrics as a Django view.
You can use django_prometheus.urls to map /metrics to this view.
"""
if "prometheus_multiproc_dir" in os.environ:
# 多進程適用,暫時不考慮
registry = prometheus_client.CollectorRegistry()
multiprocess.MultiProcessCollector(registry)
else:
# 重點是這裏
registry = prometheus_client.REGISTRY
metrics_page = prometheus_client.generate_latest(registry)
return HttpResponse(
metrics_page, content_type=prometheus_client.CONTENT_TYPE_LATEST
)
這個view函數幹了什麼事呢?它主要是生成一個registry對象,並通過這個registry對象來獲取最新的metrics頁,最後再返回這個最新的metrics頁。看到這裏大家應該能猜到了,這個metrics_page
應該包含的就是我們的metrics信息。那麼這個registry
是幹什麼的呢?
Registry
在回答這個問題之前,我們先想一下,我們怎麼收集不同接口以及不同collector的數據,是不是需要有個地方來存儲這些信息呢?那麼這個registry是不是就是這個作用呢?那麼我們再來看下prometheus_client
的源碼
# prometheus_client/registry.py 部分源碼
class CollectorRegistry(object):
"""Metric collector registry.
Collectors must have a no-argument method 'collect' that returns a list of
Metric objects. The returned metrics should be consistent with the Prometheus
exposition formats.
"""
def __init__(self, auto_describe=False, target_info=None):
self._collector_to_names = {}
self._names_to_collectors = {}
self._auto_describe = auto_describe
self._lock = Lock()
self._target_info = {}
self.set_target_info(target_info)
def register(self, collector):
"""Add a collector to the registry."""
with self._lock:
names = self._get_names(collector)
duplicates = set(self._names_to_collectors).intersection(names)
if duplicates:
raise ValueError(
'Duplicated timeseries in CollectorRegistry: {0}'.format(
duplicates))
for name in names:
# 本段代碼的核心,就是要把collector對象存儲到字典當中,從而實現註冊的功能
self._names_to_collectors[name] = collector
self._collector_to_names[collector] = names
...
# 在ExportToDjangoView中使用的REGISTRY來源於此
REGISTRY = CollectorRegistry(auto_describe=True)
首先我們清楚了ExportToDjangoView
函數中的REGISTRY
本質上也是一個CollectorRegistry
對象。而通過CollectorRegistry
的register
method,我們可以發現它實際上就是用於註冊collector對象的,它會將傳入的collector對象保存於_names_to_collectors
這個字典當中。
到這我們大體知道了django-prometheus
是如何添加/metrics
url以及在view函數中都做了些什麼。到現在還有兩個疑問沒有解決:
-
CollectorRegistry
究竟在什麼時候調用了register
函數? -
CollectorRegistry
究竟是如何獲取到相應的metrics的呢?
我們首先注意到在ExportToDjangoView
這個view函數中是通過如下語句獲取到最新的metrics的
metrics_page = prometheus_client.generate_latest(registry)
而generate_latest
的源碼看下:
def generate_latest(registry=REGISTRY):
"""Returns the metrics from the registry in latest text format as a string."""
...
output = []
# 通過調用registry.collect()可以獲取到所有collector的metrics
for metric in registry.collect():
try:
...
output.append('# HELP {0} {1}\n'.format(
mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
output.append('# TYPE {0} {1}\n'.format(mname, mtype))
om_samples = {}
for s in metric.samples:
for suffix in ['_created', '_gsum', '_gcount']:
if s.name == metric.name + suffix:
# OpenMetrics specific sample, put in a gauge at the end.
om_samples.setdefault(suffix, []).append(sample_line(s))
break
else:
output.append(sample_line(s))
except Exception as exception:
exception.args = (exception.args or ('',)) + (metric,)
raise
for suffix, lines in sorted(om_samples.items()):
output.append('# HELP {0}{1} {2}\n'.format(metric.name, suffix,
metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
output.append('# TYPE {0}{1} gauge\n'.format(metric.name, suffix))
output.extend(lines)
return ''.join(output).encode('utf-8')
從代碼中可以看出,所有的原始的metrics的獲取都是從registry.collect()
這裏得到的,得到這些原始的metrics之後,再加以格式化,格式成Prometheus規定的格式,最後拼成一頁並進行返回。
而collect()
代碼如下:
# prometheus_client/registry.py 部分源碼
def collect(self):
"""Yields metrics from the collectors in the registry."""
collectors = None
ti = None
with self._lock:
collectors = copy.copy(self._collector_to_names)
if self._target_info:
ti = self._target_info_metric()
if ti:
yield ti
# ----- 這裏是核心 -------
for collector in collectors:
for metric in collector.collect():
yield metric
# ------------------------
這段代碼的前半部分是針對target_info
,由於我們的registry在初始化的時候沒有傳遞target_info
參數,默認爲None,所以前邊這部分代碼可以忽略。後面的核心代碼可以看到就是要把註冊的collector拿出來一個一個去調用collect()
方法,從而獲取到對應collector的metrics。
從上邊這段代碼分析我們已經回答了第二個問題,即我們知道了具體收集metrics的過程,但是對於第一個問題(什麼時候register)仍然沒有找到答案。那麼猜測下在什麼時間註冊collector最合適呢?
Middleware
針對上邊的問題,初步猜測是在PrometheusBeforeMiddleware
這個中間件中,那就看看這個中間件是個啥吧,上源碼:
class PrometheusBeforeMiddleware(MiddlewareMixin):
"""Monitoring middleware that should run before other middlewares."""
metrics_cls = Metrics
def __init__(self, get_response=None):
super().__init__(get_response)
self.metrics = self.metrics_cls.get_instance()
def process_request(self, request):
self.metrics.requests_total.inc()
request.prometheus_before_middleware_event = Time()
def process_response(self, request, response):
self.metrics.responses_total.inc()
if hasattr(request, "prometheus_before_middleware_event"):
self.metrics.requests_latency_before.observe(
TimeSince(request.prometheus_before_middleware_event)
)
else:
self.metrics.requests_unknown_latency_before.inc()
return response
其中初始化方法__init__
會在項目啓動的時候就調用,這個時候就是最好的時機去做註冊相關的工作。再進一步的,發現其本質是調用了Metrics
的get_instance()
方法。再進一步看看這個Metrics
類:
class Metrics:
_instance = None
@classmethod
def get_instance(cls):
if not cls._instance:
cls._instance = cls()
return cls._instance
def register_metric(self, metric_cls, name, documentation, labelnames=(), **kwargs):
return metric_cls(name, documentation, labelnames=labelnames, **kwargs)
def __init__(self, *args, **kwargs):
self.register()
def register(self):
self.requests_total = self.register_metric(
Counter,
"django_http_requests_before_middlewares_total",
"Total count of requests before middlewares run.",
namespace=NAMESPACE,
)
...
這裏有幾點需要注意:
-
Metrics
使用了單例模式,使用時總是通過類方法get_instance()
來獲取其實例。而這個實例在初始化時只做了一件事那就是register()
,至此我們終於即將要揭開謎底了。所有的註冊工作就是在Metrics
這個類初始化的時候進行註冊的。 - 可是當我們進入
register_metric
方法中發現它只是調用對應的Collector類進行初始化。那麼是不是在collector的初始化的時候就進行了註冊呢?
Collector
帶着這個疑問,看了下prometheus_client
定義Collector的文件metrics.py
, 在這個文件中我們會發現所有類型的Collector(包括Counter, Gauge, Histogram, Summary等)都繼承自MetricWrapperBase
, 而這個基類的部分源碼如下:
# prometheus_client/metrics.py 部分源碼
class MetricWrapperBase(object):
...
def collect(self):
metric = self._get_metric()
for suffix, labels, value in self._samples():
metric.add_sample(self._name + suffix, labels, value)
return [metric]
...
def __init__(self,
name,
documentation,
labelnames=(),
namespace='',
subsystem='',
unit='',
registry=REGISTRY, # 注意這裏使用的是默認的REGISTRY
labelvalues=None,
):
self._name = _build_full_name(self._type, name, namespace, subsystem, unit)
self._labelnames = _validate_labelnames(self, labelnames)
self._labelvalues = tuple(labelvalues or ())
self._kwargs = {}
self._documentation = documentation
self._unit = unit
if not METRIC_NAME_RE.match(self._name):
raise ValueError('Invalid metric name: ' + self._name)
if self._is_parent():
# Prepare the fields needed for child metrics.
self._lock = Lock()
self._metrics = {}
if self._is_observable():
self._metric_init()
if not self._labelvalues:
# Register the multi-wrapper parent metric, or if a label-less metric, the whole shebang.
# 這裏是關鍵,至此謎底揭曉
if registry:
registry.register(self)
...
通過這部分代碼我們終於明白了其在Collector默認使用了REGISTRY
作爲註冊器,並在初始化的時候進行註冊。至此我們已經搞清楚了整個過程,再總結下:
- Django在啓動的時候會調用middleware的初始化方法。
- PrometheusBeforeMiddleware中間件會在初始化方法中調用Metrics的
get_instance()
方法 - 而這個方法在第一次初始化實例的時候(因爲是單例模式,所以也只調用一次),就會調用
register()
方法 -
register()
方法中會初始化所有的Collector,而Collector的初始化方法會自動調用register,將本Collector本身註冊到默認的REGISTRY
需要注意的是,如果是自定義的Collector,如果不指定registry,那麼默認也是REGISTRY
, 所以最終我們在調用generate_latest
的時候也會從REGISTRY
獲取到自定義的Collector。
到這裏,我們已經基本搞清楚了大部分的流程,但是如果你足夠細心的話,你會注意到ExportToDjangoView
這個view方法中還有一種情況並不是用默認的REGISTRY
,如下代碼所示:
if "prometheus_multiproc_dir" in os.environ:
registry = prometheus_client.CollectorRegistry()
multiprocess.MultiProcessCollector(registry)
- 首先自定義了一個registry,這個registry在初始化的時候沒有傳遞任何參數,因此
auto_describe
默認爲False,這是與默認的REGISTRY
的區別。 - 調用了
MultiProcessCollector
的初始化方法,並將我們創建的registry傳遞給了它的初始化方法。 - 這種情況主要是適用於多進程的時候,
django-prometheus
和prometheus_client
都針對多進程有特殊的處理。這塊作爲一個懸疑點留給有興趣的朋友,後續有機會再針對此處做詳細闡釋。