Skip to content

Commit 84c84bf

Browse files
committed
export RQ status as prometheus metrics
fixes: #503
1 parent cd05d2f commit 84c84bf

File tree

5 files changed

+98
-1
lines changed

5 files changed

+98
-1
lines changed

django_rq/metrics_collector.py

+59
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 # type: ignore[assignment, misc]

django_rq/urls.py

+6
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 [] # type: ignore[truthy-function]
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

+29-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from django.contrib import admin, messages
77
from django.contrib.admin.views.decorators import staff_member_required
8-
from django.http import Http404, JsonResponse
8+
from django.http import Http404, HttpResponse, JsonResponse
99
from django.shortcuts import redirect, render
1010
from django.urls import reverse
1111
from django.views.decorators.cache import never_cache
@@ -28,6 +28,15 @@
2828
from .settings import API_TOKEN, QUEUES_MAP
2929
from .utils import get_executions, get_jobs, get_scheduler_statistics, get_statistics, stop_jobs
3030

31+
try:
32+
import prometheus_client
33+
34+
from .metrics_collector import RQCollector
35+
except ImportError:
36+
prometheus_client = RQCollector = None # type: ignore[assignment, misc]
37+
38+
registry = None
39+
3140

3241
@never_cache
3342
@staff_member_required
@@ -49,6 +58,25 @@ def stats_json(request, token=None):
4958
)
5059

5160

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

setup.cfg

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ warn_unreachable = true
1414
[mypy-django_redis.*]
1515
ignore_missing_imports = true
1616

17+
[mypy-prometheus_client.*]
18+
ignore_missing_imports = true
19+
1720
[mypy-redis_cache.*]
1821
ignore_missing_imports = true
1922

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
},
2020
install_requires=['django>=3.2', 'rq>=2', 'redis>=3.5'],
2121
extras_require={
22+
'prometheus-metrics': ['prometheus_client>=0.4.0'],
2223
'Sentry': ['sentry-sdk>=1.0.0'],
2324
'testing': [],
2425
},

0 commit comments

Comments
 (0)