forked from numerique-gouv/impress
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨(backend) Adding /prometheus logging endpoint numerique-gouv#455
implements various metrics for users, documents and the actual django application Signed-off-by: lindenb1 <[email protected]>
- Loading branch information
Showing
9 changed files
with
191 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
from prometheus_client.core import GaugeMetricFamily | ||
from django.utils.timezone import now | ||
from django.db.models import Count, Min, Max, Q, F | ||
from datetime import timedelta | ||
from core import models | ||
from django.conf import settings | ||
|
||
|
||
class CustomMetricsExporter: | ||
""" | ||
Custom Prometheus metrics collector for user and document statistics. | ||
""" | ||
|
||
def collect(self): | ||
namespace = getattr(settings, "PROMETHEUS_METRIC_NAMESPACE", "") | ||
|
||
def prefixed_metric_name(name): | ||
return f"{namespace}_{name}" if namespace else name | ||
|
||
now_time = now() | ||
today_start_utc = now_time.replace(hour=0, minute=0, second=0, microsecond=0) | ||
one_week_ago = today_start_utc - timedelta(days=7) | ||
one_month_ago = today_start_utc - timedelta(days=30) | ||
|
||
user_count = models.User.objects.count() | ||
active_users_today = models.User.objects.filter( | ||
Q(documentaccess__updated_at__gte=today_start_utc) | | ||
Q(link_traces__created_at__gte=today_start_utc) | | ||
Q(last_login__gte=today_start_utc) | ||
).distinct().count() | ||
active_users_7_days = models.User.objects.filter( | ||
Q(documentaccess__updated_at__gte=one_week_ago) | | ||
Q(link_traces__created_at__gte=one_week_ago) | | ||
Q(last_login__gte=one_week_ago) | ||
).distinct().count() | ||
active_users_30_days = models.User.objects.filter( | ||
Q(documentaccess__updated_at__gte=one_month_ago) | | ||
Q(link_traces__created_at__gte=one_month_ago) | | ||
Q(last_login__gte=one_month_ago) | ||
).distinct().count() | ||
|
||
total_documents = models.Document.objects.count() | ||
shared_docs_count = models.Document.objects.annotate( | ||
access_count=Count("accesses") | ||
).filter(access_count__gt=1).count() | ||
active_docs_today = models.Document.objects.filter( | ||
updated_at__gte=today_start_utc, | ||
updated_at__lt=today_start_utc + timedelta(days=1), | ||
).count() | ||
active_docs_last_7_days = models.Document.objects.filter( | ||
updated_at__gte=one_week_ago | ||
).count() | ||
active_docs_last_30_days = models.Document.objects.filter( | ||
updated_at__gte=one_month_ago | ||
).count() | ||
|
||
oldest_doc_date = models.Document.objects.aggregate( | ||
oldest=Min("created_at") | ||
)["oldest"] | ||
newest_doc_date = models.Document.objects.aggregate( | ||
newest=Max("created_at") | ||
)["newest"] | ||
|
||
user_doc_counts = models.DocumentAccess.objects.values("user_id").annotate( | ||
doc_count=Count("document_id"), | ||
admin_email=F("user__admin_email") | ||
) | ||
|
||
metrics = [] | ||
metrics.append(GaugeMetricFamily(prefixed_metric_name("total_users"), "Total number of users", value=user_count)) | ||
metrics.append(GaugeMetricFamily(prefixed_metric_name("active_users_today"), "Number of active users today", value=active_users_today)) | ||
metrics.append(GaugeMetricFamily(prefixed_metric_name("active_users_7_days"), "Number of active users in the last 7 days", value=active_users_7_days)) | ||
metrics.append(GaugeMetricFamily(prefixed_metric_name("active_users_30_days"), "Number of active users in the last 30 days", value=active_users_30_days)) | ||
metrics.append(GaugeMetricFamily(prefixed_metric_name("total_documents"), "Total number of documents", value=total_documents)) | ||
metrics.append(GaugeMetricFamily(prefixed_metric_name("shared_documents"), "Number of shared documents", value=shared_docs_count)) | ||
metrics.append(GaugeMetricFamily(prefixed_metric_name("active_documents_today"), "Number of active documents today", value=active_docs_today)) | ||
metrics.append(GaugeMetricFamily(prefixed_metric_name("active_documents_7_days"), "Number of active documents in the last 7 days", value=active_docs_last_7_days)) | ||
metrics.append(GaugeMetricFamily(prefixed_metric_name("active_documents_30_days"), "Number of active documents in the last 30 days", value=active_docs_last_30_days)) | ||
|
||
if oldest_doc_date: | ||
metrics.append(GaugeMetricFamily( | ||
prefixed_metric_name("oldest_document_date"), "Timestamp of the oldest document creation date", | ||
value=oldest_doc_date.timestamp() | ||
)) | ||
if newest_doc_date: | ||
metrics.append(GaugeMetricFamily( | ||
prefixed_metric_name("newest_document_date"), "Timestamp of the newest document creation date", | ||
value=newest_doc_date.timestamp() | ||
)) | ||
|
||
user_distribution_metric = GaugeMetricFamily( | ||
prefixed_metric_name("user_document_distribution"), "Document counts per user", labels=["user_email"] | ||
) | ||
for user in user_doc_counts: | ||
if user["admin_email"]: # Validate email existence | ||
user_distribution_metric.add_metric([user["admin_email"]], user["doc_count"]) | ||
metrics.append(user_distribution_metric) | ||
|
||
for metric in metrics: | ||
yield metric |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import os | ||
from ipaddress import ip_network, ip_address | ||
from django.http import HttpResponseForbidden | ||
|
||
|
||
def cidr_protected_view(view): | ||
""" | ||
Decorator to protect a view with a CIDR filter. | ||
CIDR ranges are fetched from the environment variable `PROMETHEUS_ALLOWED_CIDR_RANGES`. | ||
If not set, access is denied by default. | ||
""" | ||
# Fetch allowed CIDR ranges from the environment variable | ||
cidr_env = os.environ.get("PROMETHEUS_ALLOWED_CIDR_RANGES", "") | ||
|
||
# validate CIDR ranges | ||
try: | ||
allowed_cidr_ranges = [ | ||
ip_network(cidr.strip().strip('"').strip("'")) | ||
for cidr in cidr_env.split(",") | ||
if cidr.strip() | ||
] | ||
except ValueError as e: | ||
raise ValueError(f"Invalid CIDR range in PROMETHEUS_ALLOWED_CIDR_RANGES: {e}") | ||
|
||
def wrapped_view(request, *args, **kwargs): | ||
# Get the client's IP address from the request | ||
client_ip = request.META.get("REMOTE_ADDR") | ||
|
||
# If no CIDR ranges are configured, deny access | ||
if not allowed_cidr_ranges: | ||
return HttpResponseForbidden("Access denied: No allowed CIDR ranges configured.") | ||
|
||
# Check if the client's IP is in the allowed CIDR ranges | ||
if not any(ip_address(client_ip) in cidr for cidr in allowed_cidr_ranges): | ||
return HttpResponseForbidden("Access denied: Your IP is not allowed.") | ||
|
||
# Proceed to the original view | ||
return view(request, *args, **kwargs) | ||
|
||
return wrapped_view |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters