背景
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
都针对多进程有特殊的处理。这块作为一个悬疑点留给有兴趣的朋友,后续有机会再针对此处做详细阐释。
References
- django_prometheus源码
- prometheus_client源码