Skip to content

Commit e7425ae

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

File tree

4 files changed

+94
-1
lines changed

4 files changed

+94
-1
lines changed

django_rq/apps.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,69 @@
11
from django.apps import AppConfig
22

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

463
class DjangoRqAdminConfig(AppConfig):
564
default_auto_field = "django.db.models.AutoField"
665
name = "django_rq"
66+
67+
def ready(self):
68+
if RQCollector is not None:
69+
REGISTRY.register(RQCollector())

django_rq/urls.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,19 @@
22

33
from . import views
44

5+
try:
6+
import rq_exporter
7+
8+
metrics_view = [
9+
re_path(r'^metrics/?$', views.prometheus_metrics, name='rq_metrics'),
10+
]
11+
except ImportError:
12+
metrics_view = []
13+
514
urlpatterns = [
615
re_path(r'^$', views.stats, name='rq_home'),
716
re_path(r'^stats.json/(?P<token>[\w]+)?/?$', views.stats_json, name='rq_home_json'),
17+
*metrics_view,
818
re_path(r'^queues/(?P<queue_index>[\d]+)/$', views.jobs, name='rq_jobs'),
919
re_path(r'^workers/(?P<queue_index>[\d]+)/$', views.workers, name='rq_workers'),
1020
re_path(r'^workers/(?P<queue_index>[\d]+)/(?P<key>[-\w\.\:\$]+)/$', views.worker_details, name='rq_worker_details'),

django_rq/views.py

Lines changed: 20 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,11 @@
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+
except ImportError:
33+
prometheus_client = None
34+
3035

3136
@never_cache
3237
@staff_member_required
@@ -48,6 +53,20 @@ def stats_json(request, token=None):
4853
)
4954

5055

56+
@never_cache
57+
@staff_member_required
58+
def prometheus_metrics(request):
59+
if not prometheus_client:
60+
raise Http404
61+
62+
registry = prometheus_client.REGISTRY
63+
encoder, content_type = prometheus_client.exposition.choose_encoder(request.META.get('HTTP_ACCEPT', ''))
64+
if 'name[]' in request.GET:
65+
registry = registry.restricted_registry(request.GET.getlist('name[]'))
66+
67+
return HttpResponse(encoder(registry), headers={'Content-Type': content_type})
68+
69+
5170
@never_cache
5271
@staff_member_required
5372
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)