Skip to content

Commit 70f07f8

Browse files
committed
export RQ status as prometheus metrics
fixes: #503
1 parent 87dd8cf commit 70f07f8

File tree

4 files changed

+95
-1
lines changed

4 files changed

+95
-1
lines changed

django_rq/metrics_collector.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from rq.job import JobStatus
2+
3+
from .queues import filter_connection_params, get_connection, get_queue, get_unique_connection_configs
4+
from .workers import get_worker_class
5+
6+
try:
7+
from prometheus_client import Summary
8+
from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily
9+
10+
class RQCollector:
11+
"""RQ stats collector"""
12+
13+
summary = Summary('rq_request_processing_seconds', 'Time spent collecting RQ data')
14+
15+
def collect(self):
16+
from .settings import QUEUES
17+
18+
with self.summary.time():
19+
rq_workers = GaugeMetricFamily('rq_workers', 'RQ workers', labels=['name', 'state', 'queues'])
20+
rq_workers_success = CounterMetricFamily('rq_workers_success', 'RQ workers success count', labels=['name', 'queues'])
21+
rq_workers_failed = CounterMetricFamily('rq_workers_failed', 'RQ workers fail count', labels=['name', 'queues'])
22+
rq_workers_working_time = CounterMetricFamily('rq_workers_working_time', 'RQ workers spent seconds', labels=['name', 'queues'])
23+
24+
rq_jobs = GaugeMetricFamily('rq_jobs', 'RQ jobs by state', labels=['queue', 'status'])
25+
26+
worker_class = get_worker_class()
27+
unique_configs = get_unique_connection_configs()
28+
connections = {}
29+
for queue_name, config in QUEUES.items():
30+
index = unique_configs.index(filter_connection_params(config))
31+
if index not in connections:
32+
connections[index] = connection = get_connection(queue_name)
33+
34+
for worker in worker_class.all(connection):
35+
name = worker.name
36+
label_queues = ','.join(worker.queue_names())
37+
rq_workers.add_metric([name, worker.get_state(), label_queues], 1)
38+
rq_workers_success.add_metric([name, label_queues], worker.successful_job_count)
39+
rq_workers_failed.add_metric([name, label_queues], worker.failed_job_count)
40+
rq_workers_working_time.add_metric([name, label_queues], worker.total_working_time)
41+
else:
42+
connection = connections[index]
43+
44+
queue = get_queue(queue_name, connection=connection)
45+
rq_jobs.add_metric([queue_name, JobStatus.QUEUED], queue.count)
46+
rq_jobs.add_metric([queue_name, JobStatus.STARTED], queue.started_job_registry.count)
47+
rq_jobs.add_metric([queue_name, JobStatus.FINISHED], queue.finished_job_registry.count)
48+
rq_jobs.add_metric([queue_name, JobStatus.FAILED], queue.failed_job_registry.count)
49+
rq_jobs.add_metric([queue_name, JobStatus.DEFERRED], queue.deferred_job_registry.count)
50+
rq_jobs.add_metric([queue_name, JobStatus.SCHEDULED], queue.scheduled_job_registry.count)
51+
52+
yield rq_workers
53+
yield rq_workers_success
54+
yield rq_workers_failed
55+
yield rq_workers_working_time
56+
yield rq_jobs
57+
58+
except ImportError:
59+
RQCollector = None

django_rq/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
from django.urls import re_path
22

33
from . import views
4+
from .metrics_collector import RQCollector
5+
6+
metrics_view = [
7+
re_path(r'^metrics/?$', views.prometheus_metrics, name='rq_metrics'),
8+
] if RQCollector else []
49

510
urlpatterns = [
611
re_path(r'^$', views.stats, name='rq_home'),
712
re_path(r'^stats.json/(?P<token>[\w]+)?/?$', views.stats_json, name='rq_home_json'),
13+
*metrics_view,
814
re_path(r'^queues/(?P<queue_index>[\d]+)/$', views.jobs, name='rq_jobs'),
915
re_path(r'^workers/(?P<queue_index>[\d]+)/$', views.workers, name='rq_workers'),
1016
re_path(r'^workers/(?P<queue_index>[\d]+)/(?P<key>[-\w\.\:\$]+)/$', views.worker_details, name='rq_worker_details'),

django_rq/views.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from django.contrib import admin, messages
66
from django.contrib.admin.views.decorators import staff_member_required
7-
from django.http import Http404, JsonResponse
7+
from django.http import Http404, HttpResponse, JsonResponse
88
from django.shortcuts import redirect, render
99
from django.urls import reverse
1010
from django.views.decorators.cache import never_cache
@@ -27,6 +27,15 @@
2727
from .settings import API_TOKEN, QUEUES_MAP
2828
from .utils import get_jobs, get_scheduler_statistics, get_statistics, stop_jobs
2929

30+
try:
31+
import prometheus_client
32+
33+
from .metrics_collector import RQCollector
34+
except ImportError:
35+
prometheus_client = RQCollector = None
36+
37+
registry = None
38+
3039

3140
@never_cache
3241
@staff_member_required
@@ -48,6 +57,25 @@ def stats_json(request, token=None):
4857
)
4958

5059

60+
@never_cache
61+
@staff_member_required
62+
def prometheus_metrics(request):
63+
global registry
64+
65+
if not RQCollector:
66+
raise Http404
67+
68+
if not registry:
69+
registry = prometheus_client.CollectorRegistry(auto_describe=True)
70+
registry.register(RQCollector())
71+
72+
encoder, content_type = prometheus_client.exposition.choose_encoder(request.META.get('HTTP_ACCEPT', ''))
73+
if 'name[]' in request.GET:
74+
registry = registry.restricted_registry(request.GET.getlist('name[]'))
75+
76+
return HttpResponse(encoder(registry), headers={'Content-Type': content_type})
77+
78+
5179
@never_cache
5280
@staff_member_required
5381
def jobs(request, queue_index):

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package_data={'': ['README.rst']},
1717
install_requires=['django>=3.2', 'rq>=1.14', 'redis>=3'],
1818
extras_require={
19+
'prometheus-metrics': ['prometheus_client>=0.4.0'],
1920
'Sentry': ['sentry-sdk>=1.0.0'],
2021
'testing': [],
2122
},

0 commit comments

Comments
 (0)